doing 1.0.92 → 2.0.5.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +19 -0
  3. data/CHANGELOG.md +596 -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 +1012 -486
  12. data/doing.fish +278 -0
  13. data/doing.gemspec +34 -0
  14. data/doing.rdoc +1759 -0
  15. data/example_plugin.rb +209 -0
  16. data/generate_completions.sh +4 -0
  17. data/img/doing-colors.jpg +0 -0
  18. data/img/doing-printf-wrap-800.jpg +0 -0
  19. data/img/doing-show-note-formatting-800.jpg +0 -0
  20. data/lib/completion/_doing.zsh +151 -0
  21. data/lib/completion/doing.bash +416 -0
  22. data/lib/completion/doing.fish +278 -0
  23. data/lib/doing/array.rb +8 -0
  24. data/lib/doing/cli_status.rb +66 -0
  25. data/lib/doing/colors.rb +136 -0
  26. data/lib/doing/configuration.rb +312 -0
  27. data/lib/doing/errors.rb +102 -0
  28. data/lib/doing/hash.rb +31 -0
  29. data/lib/doing/hooks.rb +59 -0
  30. data/lib/doing/item.rb +155 -0
  31. data/lib/doing/log_adapter.rb +342 -0
  32. data/lib/doing/markdown_document_listener.rb +174 -0
  33. data/lib/doing/note.rb +59 -0
  34. data/lib/doing/pager.rb +95 -0
  35. data/lib/doing/plugin_manager.rb +208 -0
  36. data/lib/doing/plugins/export/csv_export.rb +48 -0
  37. data/lib/doing/plugins/export/html_export.rb +83 -0
  38. data/lib/doing/plugins/export/json_export.rb +140 -0
  39. data/lib/doing/plugins/export/markdown_export.rb +85 -0
  40. data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
  41. data/lib/doing/plugins/export/template_export.rb +141 -0
  42. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  43. data/lib/doing/plugins/import/calendar_import.rb +76 -0
  44. data/lib/doing/plugins/import/doing_import.rb +144 -0
  45. data/lib/doing/plugins/import/timing_import.rb +78 -0
  46. data/lib/doing/string.rb +347 -0
  47. data/lib/doing/symbol.rb +16 -0
  48. data/lib/doing/time.rb +18 -0
  49. data/lib/doing/util.rb +186 -0
  50. data/lib/doing/version.rb +1 -1
  51. data/lib/doing/wwid.rb +1868 -2356
  52. data/lib/doing/wwidfile.rb +117 -0
  53. data/lib/doing.rb +44 -4
  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 +210 -0
  69. data/scripts/generate_fish_completions.rb +201 -0
  70. data/scripts/generate_zsh_completions.rb +164 -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,347 @@
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)
213
+ replace tag(tag, value: value, remove: remove, rename_to: rename_to, regex: regex)
214
+ end
215
+
216
+ def tag(tag, value: nil, remove: false, rename_to: nil, regex: false)
217
+ title = dup
218
+ title.chomp!
219
+ tag = tag.sub(/^@?/, '')
220
+ case_sensitive = tag !~ /[A-Z]/
221
+
222
+ rx_tag = if regex
223
+ tag.gsub(/\./, '\S')
224
+ else
225
+ tag.gsub(/\?/, '.').gsub(/\*/, '\S*?')
226
+ end
227
+
228
+ if remove || rename_to
229
+ return title unless title =~ /#{rx_tag}(?=[ (]|$)/
230
+
231
+ rx = Regexp.new("(^| )@#{rx_tag}(\\([^)]*\\))?(?= |$)", case_sensitive)
232
+ if title =~ rx
233
+ title.gsub!(rx) do
234
+ m = Regexp.last_match
235
+ rename_to ? "#{m[1]}@#{rename_to}#{m[2]}" : m[1]
236
+ end
237
+
238
+ title.dedup_tags!
239
+ title.chomp!
240
+
241
+ if rename_to
242
+ f = "@#{tag}".cyan
243
+ t = "@#{rename_to}".cyan
244
+ Doing.logger.debug('Tag:', %(renamed #{f} to #{t} in "#{title}"))
245
+ else
246
+ f = "@#{tag}".cyan
247
+ Doing.logger.debug('Tag:', %(removed #{f} from "#{title}"))
248
+ end
249
+ else
250
+ Doing.logger.debug('Skipped:', "not tagged #{"@#{tag}".cyan}")
251
+ end
252
+ elsif title =~ /@#{tag}(?=[ (]|$)/
253
+ Doing.logger.debug('Skipped:', "already tagged #{"@#{tag}".cyan}")
254
+ return title
255
+ else
256
+ add = tag
257
+ add += "(#{value})" unless value.nil?
258
+ title.chomp!
259
+ title += " @#{add}"
260
+
261
+ title.dedup_tags!
262
+ title.chomp!
263
+ # Doing.logger.debug('Added tag:', %(#{('@' + tag).cyan} to "#{title}"))
264
+ end
265
+
266
+ title.gsub(/ +/, ' ')
267
+ end
268
+
269
+ ##
270
+ ## @brief Remove duplicate tags, leaving only first occurrence
271
+ ##
272
+ ## @return Deduplicated string
273
+ ##
274
+ def dedup_tags!
275
+ replace dedup_tags
276
+ end
277
+
278
+ def dedup_tags
279
+ title = dup
280
+ tags = title.scan(/(?<=^| )(@(\S+?)(\([^)]+\))?)(?= |$)/).uniq
281
+ tags.each do |tag|
282
+ found = false
283
+ title.gsub!(/( |^)#{tag[1]}(\([^)]+\))?(?= |$)/) do |m|
284
+ if found
285
+ ''
286
+ else
287
+ found = true
288
+ m
289
+ end
290
+ end
291
+ end
292
+ title
293
+ end
294
+
295
+ ##
296
+ ## @brief Turn raw urls into HTML links
297
+ ##
298
+ ## @param opt (Hash) Additional Options
299
+ ##
300
+ def link_urls!(opt = {})
301
+ replace link_urls(opt)
302
+ end
303
+
304
+ def link_urls(opt = {})
305
+ opt[:format] ||= :html
306
+ str = self.dup
307
+
308
+ if :format == :markdown
309
+ # Remove <self-linked> formatting
310
+ str.gsub!(/<(.*?)>/) do |match|
311
+ m = Regexp.last_match
312
+ if m[1] =~ /^https?:/
313
+ m[1]
314
+ else
315
+ match
316
+ end
317
+ end
318
+ end
319
+
320
+ # Replace qualified urls
321
+ str.gsub!(%r{(?mi)(?<!["'\[(\\])((http|https)://)([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?}) do |_match|
322
+ m = Regexp.last_match
323
+ proto = m[1].nil? ? 'http://' : ''
324
+ case opt[:format]
325
+ when :html
326
+ %(<a href="#{proto}#{m[0]}" title="Link to #{m[0].sub(/^https?:\/\//, '')}">[#{m[3]}]</a>)
327
+ when :markdown
328
+ "[#{m[0]}](#{proto}#{m[0]})"
329
+ else
330
+ m[0]
331
+ end
332
+ end
333
+
334
+ # Clean up unlinked <urls>
335
+ str.gsub!(/<(\w+:.*?)>/) do |match|
336
+ m = Regexp.last_match
337
+ if m[1] =~ /<a href/
338
+ match
339
+ else
340
+ %(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
341
+ end
342
+ end
343
+
344
+ str
345
+ end
346
+ end
347
+ 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.92'
2
+ VERSION = '2.0.5.pre'
3
3
  end