doing 1.0.93 → 2.0.6.pre

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG.md +616 -0
  4. data/COMMANDS.md +1181 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +110 -0
  7. data/LICENSE +23 -0
  8. data/README.md +15 -699
  9. data/Rakefile +79 -0
  10. data/_config.yml +1 -0
  11. data/bin/doing +1055 -494
  12. data/doing.gemspec +34 -0
  13. data/doing.rdoc +1839 -0
  14. data/example_plugin.rb +209 -0
  15. data/generate_completions.sh +5 -0
  16. data/img/doing-colors.jpg +0 -0
  17. data/img/doing-printf-wrap-800.jpg +0 -0
  18. data/img/doing-show-note-formatting-800.jpg +0 -0
  19. data/lib/completion/_doing.zsh +203 -0
  20. data/lib/completion/doing.bash +449 -0
  21. data/lib/completion/doing.fish +329 -0
  22. data/lib/doing/array.rb +8 -0
  23. data/lib/doing/cli_status.rb +70 -0
  24. data/lib/doing/colors.rb +136 -0
  25. data/lib/doing/configuration.rb +312 -0
  26. data/lib/doing/errors.rb +109 -0
  27. data/lib/doing/hash.rb +31 -0
  28. data/lib/doing/hooks.rb +59 -0
  29. data/lib/doing/item.rb +155 -0
  30. data/lib/doing/log_adapter.rb +344 -0
  31. data/lib/doing/markdown_document_listener.rb +174 -0
  32. data/lib/doing/note.rb +59 -0
  33. data/lib/doing/pager.rb +95 -0
  34. data/lib/doing/plugin_manager.rb +208 -0
  35. data/lib/doing/plugins/export/csv_export.rb +48 -0
  36. data/lib/doing/plugins/export/html_export.rb +83 -0
  37. data/lib/doing/plugins/export/json_export.rb +140 -0
  38. data/lib/doing/plugins/export/markdown_export.rb +85 -0
  39. data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
  40. data/lib/doing/plugins/export/template_export.rb +141 -0
  41. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  42. data/lib/doing/plugins/import/calendar_import.rb +76 -0
  43. data/lib/doing/plugins/import/doing_import.rb +144 -0
  44. data/lib/doing/plugins/import/timing_import.rb +78 -0
  45. data/lib/doing/string.rb +348 -0
  46. data/lib/doing/symbol.rb +16 -0
  47. data/lib/doing/time.rb +18 -0
  48. data/lib/doing/util.rb +186 -0
  49. data/lib/doing/version.rb +1 -1
  50. data/lib/doing/wwid.rb +1868 -2349
  51. data/lib/doing/wwidfile.rb +117 -0
  52. data/lib/doing.rb +43 -3
  53. data/lib/examples/commands/autotag.rb +63 -0
  54. data/lib/examples/commands/wiki.rb +81 -0
  55. data/lib/examples/plugins/hooks.rb +22 -0
  56. data/lib/examples/plugins/say_export.rb +202 -0
  57. data/lib/examples/plugins/templates/wiki.css +169 -0
  58. data/lib/examples/plugins/templates/wiki.haml +27 -0
  59. data/lib/examples/plugins/templates/wiki_index.haml +18 -0
  60. data/lib/examples/plugins/wiki_export.rb +87 -0
  61. data/lib/templates/doing-markdown.erb +5 -0
  62. data/man/doing.1 +964 -0
  63. data/man/doing.1.html +711 -0
  64. data/man/doing.1.ronn +600 -0
  65. data/package-lock.json +3 -0
  66. data/rdoc_to_mmd.rb +42 -0
  67. data/rdocfixer.rb +13 -0
  68. data/scripts/generate_bash_completions.rb +211 -0
  69. data/scripts/generate_fish_completions.rb +204 -0
  70. data/scripts/generate_zsh_completions.rb +168 -0
  71. metadata +82 -7
  72. data/lib/doing/helpers.rb +0 -191
  73. data/lib/doing/markdown_export.rb +0 -16
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## @brief String helpers
6
+ ##
7
+ class ::String
8
+ include Doing::Color
9
+ def to_rx(distance)
10
+ gsub(/(.)/, "\\1.{0,#{distance}}")
11
+ end
12
+
13
+ def truthy?
14
+ if self =~ /^(0|f(alse)?|n(o)?)$/i
15
+ false
16
+ else
17
+ true
18
+ end
19
+ end
20
+
21
+ def highlight_tags!(color = 'yellow')
22
+ replace highlight_tags(color)
23
+ end
24
+
25
+ def highlight_tags(color = 'yellow')
26
+ escapes = scan(/(\e\[[\d;]+m)[^\e]+@/)
27
+ tag_color = Doing::Color.send(color)
28
+ last_color = if !escapes.empty?
29
+ escapes[-1][0]
30
+ else
31
+ Doing::Color.default
32
+ end
33
+ gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{last_color}")
34
+ end
35
+
36
+ ##
37
+ ## @brief Test if line should be ignored
38
+ ##
39
+ ## @return [Boolean] line is empty or comment
40
+ ##
41
+ def ignore?
42
+ line = self
43
+ line =~ /^#/ || line =~ /^\s*$/
44
+ end
45
+
46
+ ##
47
+ ## @brief Truncate to nearest word
48
+ ##
49
+ ## @param len The length
50
+ ##
51
+ def truncate(len, ellipsis: '...')
52
+ return self if length <= len
53
+
54
+ total = 0
55
+ res = []
56
+
57
+ split(/ /).each do |word|
58
+ break if total + 1 + word.length > len
59
+
60
+ total += 1 + word.length
61
+ res.push(word)
62
+ end
63
+ res.join(' ') + ellipsis
64
+ end
65
+
66
+ def truncate!(len, ellipsis: '...')
67
+ replace truncate(len, ellipsis: ellipsis)
68
+ end
69
+
70
+ ##
71
+ ## @brief Truncate string in the middle
72
+ ##
73
+ ## @param len The length
74
+ ## @param ellipsis The ellipsis
75
+ ##
76
+ def truncmiddle(len, ellipsis: '...')
77
+ return self if length <= len
78
+ len -= (ellipsis.length / 2).to_i
79
+ total = length
80
+ half = total / 2
81
+ cut = (total - len) / 2
82
+ sub(/(.{#{half - cut}}).*?(.{#{half - cut}})$/, "\\1#{ellipsis}\\2")
83
+ end
84
+
85
+ def truncmiddle!(len, ellipsis: '...')
86
+ replace truncmiddle(len, ellipsis: ellipsis)
87
+ end
88
+
89
+ ##
90
+ ## @brief Remove color escape codes
91
+ ##
92
+ ## @return clean string
93
+ ##
94
+ def uncolor
95
+ gsub(/\e\[[\d;]+m/,'')
96
+ end
97
+
98
+ def uncolor!
99
+ replace uncolor
100
+ end
101
+
102
+ ##
103
+ ## @brief Wrap string at word breaks, respecting tags
104
+ ##
105
+ ## @param len [Integer] The length
106
+ ## @param offset [Integer] (Optional) The width to pad each subsequent line
107
+ ## @param prefix [String] (Optional) A prefix to add to each line
108
+ ##
109
+ def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', after: '', reset: '')
110
+ note_rx = /(?i-m)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)/
111
+ str = gsub(/@\w+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
112
+ words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
113
+ out = []
114
+ line = []
115
+ words.each do |word|
116
+ if line.join(' ').length + word.length + 1 > len
117
+ out.push(line.join(' '))
118
+ line.clear
119
+ end
120
+
121
+ line << word
122
+ end
123
+ out.push(line.join(' '))
124
+ note = ''
125
+ after.sub!(note_rx) do
126
+ note = Regexp.last_match(0)
127
+ ''
128
+ end
129
+
130
+ out[0] = format("%-#{pad}s%s", out[0], after)
131
+ left_pad = ' ' * (offset)
132
+ left_pad += indent
133
+ out.map { |l| "#{left_pad}#{prefix}#{l}" }.join("\n").strip + " #{note}".chomp
134
+ end
135
+
136
+ ##
137
+ ## @brief Capitalize on the first character on string
138
+ ##
139
+ ## @return Capitalized string
140
+ ##
141
+ def cap_first
142
+ sub(/^\w/) do |m|
143
+ m.upcase
144
+ end
145
+ end
146
+
147
+ ##
148
+ ## @brief Convert a sort order string to a qualified type
149
+ ##
150
+ ## @return (String) 'asc' or 'desc'
151
+ ##
152
+ def normalize_order!
153
+ replace normalize_order
154
+ end
155
+
156
+ def normalize_order(default = 'asc')
157
+ case self
158
+ when /^a/i
159
+ 'asc'
160
+ when /^d/i
161
+ 'desc'
162
+ else
163
+ default
164
+ end
165
+ end
166
+
167
+ ##
168
+ ## @brief Convert a boolean string to a symbol
169
+ ##
170
+ ## @return Symbol :and, :or, or :not
171
+ ##
172
+ def normalize_bool!
173
+ replace normalize_bool
174
+ end
175
+
176
+ def normalize_bool(default = :and)
177
+ case self
178
+ when /(and|all)/i
179
+ :and
180
+ when /(any|or)/i
181
+ :or
182
+ when /(not|none)/i
183
+ :not
184
+ else
185
+ default.is_a?(Symbol) ? default : default.normalize_bool
186
+ end
187
+ end
188
+
189
+ def normalize_trigger!
190
+ replace normalize_trigger
191
+ end
192
+
193
+ def normalize_trigger
194
+ gsub(/\((?!\?:)/, '(?:').downcase
195
+ end
196
+
197
+ def to_tags
198
+ gsub(/ *, */, ' ').gsub(/ +/, ' ').split(/ /).sort.uniq.map { |t| t.strip.sub(/^@/, '') }
199
+ end
200
+
201
+ def add_tags!(tags, remove: false)
202
+ replace add_tags(tags, remove: remove)
203
+ end
204
+
205
+ def add_tags(tags, remove: false)
206
+ title = self.dup
207
+ tags = tags.to_tags if tags.is_a?(String)
208
+ tags.each { |tag| title.tag!(tag, remove: remove) }
209
+ title
210
+ end
211
+
212
+ def tag!(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false)
213
+ replace tag(tag, value: value, remove: remove, rename_to: rename_to, regex: regex, single: single)
214
+ end
215
+
216
+ def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false)
217
+ log_level = single ? :info : :debug
218
+ title = dup
219
+ title.chomp!
220
+ tag = tag.sub(/^@?/, '')
221
+ case_sensitive = tag !~ /[A-Z]/
222
+
223
+ rx_tag = if regex
224
+ tag.gsub(/\./, '\S')
225
+ else
226
+ tag.gsub(/\?/, '.').gsub(/\*/, '\S*?')
227
+ end
228
+
229
+ if remove || rename_to
230
+ return title unless title =~ /#{rx_tag}(?=[ (]|$)/
231
+
232
+ rx = Regexp.new("(^| )@#{rx_tag}(\\([^)]*\\))?(?= |$)", case_sensitive)
233
+ if title =~ rx
234
+ title.gsub!(rx) do
235
+ m = Regexp.last_match
236
+ rename_to ? "#{m[1]}@#{rename_to}#{m[2]}" : m[1]
237
+ end
238
+
239
+ title.dedup_tags!
240
+ title.chomp!
241
+
242
+ if rename_to
243
+ f = "@#{tag}".cyan
244
+ t = "@#{rename_to}".cyan
245
+ Doing.logger.write(log_level, 'Tag:', %(renamed #{f} to #{t} in "#{title}"))
246
+ else
247
+ f = "@#{tag}".cyan
248
+ Doing.logger.write(log_level, 'Tag:', %(removed #{f} from "#{title}"))
249
+ end
250
+ else
251
+ Doing.logger.debug('Skipped:', "not tagged #{"@#{tag}".cyan}")
252
+ end
253
+ elsif title =~ /@#{tag}(?=[ (]|$)/
254
+ Doing.logger.debug('Skipped:', "already tagged #{"@#{tag}".cyan}")
255
+ return title
256
+ else
257
+ add = tag
258
+ add += "(#{value})" unless value.nil?
259
+ title.chomp!
260
+ title += " @#{add}"
261
+
262
+ title.dedup_tags!
263
+ title.chomp!
264
+ Doing.logger.write(log_level, 'Tag:', %(added #{('@' + tag).cyan} to "#{title}"))
265
+ end
266
+
267
+ title.gsub(/ +/, ' ')
268
+ end
269
+
270
+ ##
271
+ ## @brief Remove duplicate tags, leaving only first occurrence
272
+ ##
273
+ ## @return Deduplicated string
274
+ ##
275
+ def dedup_tags!
276
+ replace dedup_tags
277
+ end
278
+
279
+ def dedup_tags
280
+ title = dup
281
+ tags = title.scan(/(?<=^| )(@(\S+?)(\([^)]+\))?)(?= |$)/).uniq
282
+ tags.each do |tag|
283
+ found = false
284
+ title.gsub!(/( |^)#{tag[1]}(\([^)]+\))?(?= |$)/) do |m|
285
+ if found
286
+ ''
287
+ else
288
+ found = true
289
+ m
290
+ end
291
+ end
292
+ end
293
+ title
294
+ end
295
+
296
+ ##
297
+ ## @brief Turn raw urls into HTML links
298
+ ##
299
+ ## @param opt (Hash) Additional Options
300
+ ##
301
+ def link_urls!(opt = {})
302
+ replace link_urls(opt)
303
+ end
304
+
305
+ def link_urls(opt = {})
306
+ opt[:format] ||= :html
307
+ str = self.dup
308
+
309
+ if :format == :markdown
310
+ # Remove <self-linked> formatting
311
+ str.gsub!(/<(.*?)>/) do |match|
312
+ m = Regexp.last_match
313
+ if m[1] =~ /^https?:/
314
+ m[1]
315
+ else
316
+ match
317
+ end
318
+ end
319
+ end
320
+
321
+ # Replace qualified urls
322
+ str.gsub!(%r{(?mi)(?<!["'\[(\\])((http|https)://)([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) do |_match|
323
+ m = Regexp.last_match
324
+ proto = m[1].nil? ? 'http://' : ''
325
+ case opt[:format]
326
+ when :html
327
+ %(<a href="#{proto}#{m[0]}" title="Link to #{m[0].sub(/^https?:\/\//, '')}">[#{m[3]}]</a>)
328
+ when :markdown
329
+ "[#{m[0]}](#{proto}#{m[0]})"
330
+ else
331
+ m[0]
332
+ end
333
+ end
334
+
335
+ # Clean up unlinked <urls>
336
+ str.gsub!(/<(\w+:.*?)>/) do |match|
337
+ m = Regexp.last_match
338
+ if m[1] =~ /<a href/
339
+ match
340
+ else
341
+ %(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
342
+ end
343
+ end
344
+
345
+ str
346
+ end
347
+ end
348
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## @brief Symbol helpers
6
+ ##
7
+ class ::Symbol
8
+ def normalize_bool
9
+ to_s.normalize_bool
10
+ end
11
+
12
+ def normalize_order
13
+ to_s.normalize_order
14
+ end
15
+ end
16
+ end
data/lib/doing/time.rb ADDED
@@ -0,0 +1,18 @@
1
+ module Doing
2
+ ##
3
+ ## @brief Date helpers
4
+ ##
5
+ class ::Time
6
+ def relative_date
7
+ if self > Date.today.to_time
8
+ strftime('%_I:%M%P')
9
+ elsif self > (Date.today - 6).to_time
10
+ strftime('%a %_I:%M%P')
11
+ elsif self.year == Date.today.year
12
+ strftime('%m/%d %_I:%M%P')
13
+ else
14
+ strftime('%m/%d/%Y %_I:%M%P')
15
+ end
16
+ end
17
+ end
18
+ end
data/lib/doing/util.rb ADDED
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Utilities
5
+ module Util
6
+ extend self
7
+
8
+ def user_home
9
+ if Dir.respond_to?('home')
10
+ Dir.home
11
+ else
12
+ File.expand_path('~')
13
+ end
14
+ end
15
+
16
+ ##
17
+ ## @brief Test if command line tool is available
18
+ ##
19
+ ## @param cli (String) The name or path of the cli
20
+ ##
21
+ def exec_available(cli)
22
+ return false if cli.nil?
23
+
24
+ if File.exist?(File.expand_path(cli))
25
+ File.executable?(File.expand_path(cli))
26
+ else
27
+ system "which #{cli}", out: File::NULL, err: File::NULL
28
+ end
29
+ end
30
+
31
+ def merge_default_proc(target, overwrite)
32
+ return unless target.is_a?(Hash) && overwrite.is_a?(Hash) && target.default_proc.nil?
33
+
34
+ target.default_proc = overwrite.default_proc
35
+ end
36
+
37
+ def duplicate_frozen_values(target)
38
+ target.each do |key, val|
39
+ target[key] = val.dup if val.frozen? && duplicable?(val)
40
+ end
41
+ end
42
+
43
+ # Non-destructive version of deep_merge_hashes! See that method.
44
+ #
45
+ # Returns the merged hashes.
46
+ def deep_merge_hashes(master_hash, other_hash)
47
+ deep_merge_hashes!(master_hash.dup, other_hash)
48
+ end
49
+
50
+ # Merges a master hash with another hash, recursively.
51
+ #
52
+ # master_hash - the "parent" hash whose values will be overridden
53
+ # other_hash - the other hash whose values will be persisted after the merge
54
+ #
55
+ # This code was lovingly stolen from some random gem:
56
+ # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
57
+ #
58
+ # Thanks to whoever made it.
59
+ def deep_merge_hashes!(target, overwrite)
60
+ merge_values(target, overwrite)
61
+ merge_default_proc(target, overwrite)
62
+ duplicate_frozen_values(target)
63
+
64
+ target
65
+ end
66
+
67
+ def duplicable?(obj)
68
+ case obj
69
+ when nil, false, true, Symbol, Numeric
70
+ false
71
+ else
72
+ true
73
+ end
74
+ end
75
+
76
+ def mergable?(value)
77
+ value.is_a?(Hash)
78
+ end
79
+
80
+ def merge_values(target, overwrite)
81
+ target.merge!(overwrite) do |_key, old_val, new_val|
82
+ if new_val.nil?
83
+ old_val
84
+ elsif mergable?(old_val) && mergable?(new_val)
85
+ deep_merge_hashes(old_val, new_val)
86
+ else
87
+ new_val
88
+ end
89
+ end
90
+ end
91
+
92
+ ##
93
+ ## @brief Write content to a file
94
+ ##
95
+ ## @param file (String) The path to the file to (over)write
96
+ ## @param content (String) The content to write to the file
97
+ ## @param backup (Boolean) create a ~ backup
98
+ ##
99
+ def write_to_file(file, content, backup: true)
100
+ unless file
101
+ puts content
102
+ return
103
+ end
104
+
105
+ file = File.expand_path(file)
106
+
107
+ if File.exist?(file) && backup
108
+ # Create a backup copy for the undo command
109
+ FileUtils.cp(file, "#{file}~")
110
+ end
111
+
112
+ File.open(file, 'w+') do |f|
113
+ f.puts content
114
+ end
115
+
116
+ Hooks.trigger :post_write, file
117
+ end
118
+
119
+ def safe_load_file(filename)
120
+ SafeYAML.load_file(filename) || {}
121
+ end
122
+
123
+ def default_editor
124
+ @default_editor = find_default_editor
125
+ end
126
+
127
+ def editor_with_args
128
+ args_for_editor(default_editor)
129
+ end
130
+
131
+ def args_for_editor(editor)
132
+ return editor if editor =~ /-\S/
133
+
134
+ args = case editor
135
+ when /^(subl|code|mate)$/
136
+ ['-w']
137
+ when /^(vim|mvim)$/
138
+ ['-f']
139
+ else
140
+ []
141
+ end
142
+ "#{editor} #{args.join(' ')}"
143
+ end
144
+
145
+ def find_default_editor(editor_for = 'default')
146
+ if ENV['DOING_EDITOR_TEST']
147
+ return ENV['EDITOR']
148
+ end
149
+
150
+ editor_config = Doing.config.settings['editors']
151
+
152
+ if editor_config.is_a?(String)
153
+ Doing.logger.warn('Deprecated:', "Please update your configuration, 'editors' should be a mapping. Delete the key and run `doing config --update`.")
154
+ return editor_config
155
+ end
156
+
157
+ if editor_config[editor_for]
158
+ editor = editor_config[editor_for]
159
+ Doing.logger.debug('Editor:', "Using #{editor} from config 'editors->#{editor_for}'")
160
+ return editor unless editor.nil? || editor.empty?
161
+ end
162
+
163
+ if editor_for != 'editor' && editor_config['default']
164
+ editor = editor_config['default']
165
+ Doing.logger.debug('Editor:', "Using #{editor} from config: 'editors->default'")
166
+ return editor unless editor.nil? || editor.empty?
167
+ end
168
+
169
+ editor ||= ENV['DOING_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
170
+
171
+ unless editor.nil? || editor.empty?
172
+ Doing.logger.debug('Editor:', "Found editor in environment variables: #{editor}")
173
+ return editor
174
+ end
175
+
176
+ Doing.logger.debug('ENV:', 'No EDITOR environment variable, testing available editors')
177
+ editors = %w[vim vi code subl mate mvim nano emacs]
178
+ editors.each do |ed|
179
+ return ed if exec_available(ed)
180
+ Doing.logger.debug('ENV:', "#{ed} not available")
181
+ end
182
+
183
+ nil
184
+ end
185
+ end
186
+ end
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '1.0.93'
2
+ VERSION = '2.0.6.pre'
3
3
  end