doing 2.0.25 → 2.1.0pre

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +18 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +28 -0
  6. data/Gemfile.lock +8 -1
  7. data/README.md +1 -1
  8. data/Rakefile +23 -4
  9. data/bin/doing +205 -127
  10. data/doc/Array.html +354 -1
  11. data/doc/Doing/Color.html +104 -92
  12. data/doc/Doing/Completion.html +216 -0
  13. data/doc/Doing/Configuration.html +340 -5
  14. data/doc/Doing/Content.html +229 -0
  15. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  16. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  17. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  18. data/doc/Doing/Errors/EmptyInput.html +1 -1
  19. data/doc/Doing/Errors/NoResults.html +1 -1
  20. data/doc/Doing/Errors/PluginException.html +1 -1
  21. data/doc/Doing/Errors/UserCancelled.html +1 -1
  22. data/doc/Doing/Errors/WrongCommand.html +1 -1
  23. data/doc/Doing/Errors.html +1 -1
  24. data/doc/Doing/Hooks.html +1 -1
  25. data/doc/Doing/Item.html +337 -49
  26. data/doc/Doing/Items.html +444 -35
  27. data/doc/Doing/LogAdapter.html +139 -51
  28. data/doc/Doing/Note.html +253 -22
  29. data/doc/Doing/Pager.html +74 -36
  30. data/doc/Doing/Plugins.html +1 -1
  31. data/doc/Doing/Prompt.html +674 -0
  32. data/doc/Doing/Section.html +354 -0
  33. data/doc/Doing/Util.html +57 -1
  34. data/doc/Doing/WWID.html +477 -670
  35. data/doc/Doing/WWIDFile.html +398 -0
  36. data/doc/Doing.html +5 -5
  37. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  38. data/doc/GLI/Commands.html +1 -1
  39. data/doc/GLI.html +1 -1
  40. data/doc/Hash.html +97 -1
  41. data/doc/Status.html +37 -3
  42. data/doc/String.html +599 -23
  43. data/doc/Symbol.html +3 -3
  44. data/doc/Time.html +1 -1
  45. data/doc/_index.html +22 -1
  46. data/doc/class_list.html +1 -1
  47. data/doc/file.README.html +8 -2
  48. data/doc/index.html +8 -2
  49. data/doc/method_list.html +453 -173
  50. data/doc/top-level-namespace.html +1 -1
  51. data/doing.gemspec +3 -0
  52. data/doing.rdoc +40 -12
  53. data/example_plugin.rb +3 -3
  54. data/lib/completion/_doing.zsh +1 -1
  55. data/lib/completion/doing.bash +8 -8
  56. data/lib/completion/doing.fish +1 -1
  57. data/lib/doing/array.rb +36 -0
  58. data/lib/doing/colors.rb +70 -66
  59. data/lib/doing/completion.rb +6 -0
  60. data/lib/doing/configuration.rb +69 -28
  61. data/lib/doing/hash.rb +37 -0
  62. data/lib/doing/item.rb +77 -12
  63. data/lib/doing/items.rb +125 -0
  64. data/lib/doing/log_adapter.rb +55 -3
  65. data/lib/doing/note.rb +53 -1
  66. data/lib/doing/pager.rb +49 -38
  67. data/lib/doing/plugins/export/markdown_export.rb +4 -4
  68. data/lib/doing/plugins/export/template_export.rb +2 -2
  69. data/lib/doing/plugins/import/calendar_import.rb +4 -4
  70. data/lib/doing/plugins/import/doing_import.rb +5 -7
  71. data/lib/doing/plugins/import/timing_import.rb +3 -3
  72. data/lib/doing/prompt.rb +206 -0
  73. data/lib/doing/section.rb +30 -0
  74. data/lib/doing/string.rb +103 -27
  75. data/lib/doing/util.rb +14 -6
  76. data/lib/doing/version.rb +1 -1
  77. data/lib/doing/wwid.rb +306 -621
  78. data/lib/doing.rb +6 -2
  79. data/lib/examples/plugins/capture_thing_import.rb +162 -0
  80. metadata +73 -5
  81. data/lib/doing/wwidfile.rb +0 -117
@@ -34,9 +34,7 @@ module Doing
34
34
  tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
35
35
  prefix = options[:prefix] || ''
36
36
 
37
- @old_items = []
38
-
39
- wwid.content.each { |_, v| @old_items.concat(v[:items]) }
37
+ @old_items = wwid.content.dup
40
38
 
41
39
  new_items = read_doing_file(path)
42
40
 
@@ -46,7 +44,7 @@ module Doing
46
44
  new_items = wwid.filter_items(new_items, opt: options)
47
45
 
48
46
  skipped = total - new_items.count
49
- Doing.logger.debug('Skipped:' , %(#{skipped} items that didn't match filter criteria)) if skipped.positive?
47
+ Doing.logger.debug('Skipped:', %(#{skipped} items that didn't match filter criteria)) if skipped.positive?
50
48
 
51
49
  imported = []
52
50
 
@@ -76,13 +74,13 @@ module Doing
76
74
  dups = new_items.count - imported.count
77
75
  Doing.logger.info('Skipped:', %(#{dups} duplicate items)) if dups.positive?
78
76
 
79
- imported = wwid.dedup(imported, !options[:overlap])
77
+ imported = wwid.dedup(imported, no_overlap: !options[:overlap])
80
78
  overlaps = new_items.count - imported.count - dups
81
79
  Doing.logger.debug('Skipped:', "#{overlaps} items with overlapping times") if overlaps.positive?
82
80
 
83
81
  imported.each do |item|
84
- wwid.add_section(item.section) unless wwid.content.key?(item.section)
85
- wwid.content[item.section][:items].push(item)
82
+ wwid.content.add_section(item.section) unless wwid.content.section?(item.section)
83
+ wwid.content.push(item)
86
84
  end
87
85
 
88
86
  Doing.logger.info('Imported:', "#{imported.count} items")
@@ -27,7 +27,7 @@ module Doing
27
27
  section = options[:section] || wwid.config['current_section']
28
28
  options[:no_overlap] ||= false
29
29
  options[:autotag] ||= wwid.auto_tag
30
- wwid.add_section(section) unless wwid.content.key?(section)
30
+ wwid.content.add_section(section) unless wwid.content.section?(section)
31
31
 
32
32
  add_tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
33
33
  prefix = options[:prefix] || '[Timing.app]'
@@ -73,11 +73,11 @@ module Doing
73
73
  filtered = skipped - new_items.count
74
74
  Doing.logger.debug('Skipped:' , %(#{filtered} items that didn't match filter criteria)) if filtered.positive?
75
75
 
76
- new_items = wwid.dedup(new_items, options[:no_overlap])
76
+ new_items = wwid.dedup(new_items, no_overlap: options[:no_overlap])
77
77
  dups = filtered - new_items.count
78
78
  Doing.logger.debug('Skipped:' , %(#{dups} items with overlapping times)) if dups.positive?
79
79
 
80
- wwid.content[section][:items].concat(new_items)
80
+ wwid.content.concat(new_items)
81
81
  Doing.logger.info('Imported:', %(#{new_items.count} items to #{section}))
82
82
  end
83
83
 
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Terminal Prompt methods
5
+ module Prompt
6
+ class << self
7
+ attr_writer :force_answer, :default_answer
8
+
9
+ include Color
10
+
11
+ def force_answer
12
+ @force_answer ||= nil
13
+ end
14
+
15
+ def default_answer
16
+ @default_answer ||= false
17
+ end
18
+
19
+ ##
20
+ ## Ask a yes or no question in the terminal
21
+ ##
22
+ ## @param question [String] The question
23
+ ## to ask
24
+ ## @param default_response (Bool) default
25
+ ## response if no input
26
+ ##
27
+ ## @return (Bool) yes or no
28
+ ##
29
+ def yn(question, default_response: false)
30
+ unless @force_answer.nil?
31
+ return @force_answer
32
+ end
33
+
34
+ default = if default_response.is_a?(String)
35
+ default_response =~ /y/i ? true : false
36
+ else
37
+ default_response
38
+ end
39
+
40
+ # if global --default is set, answer default
41
+ return default if @default_answer
42
+
43
+ # if this isn't an interactive shell, answer default
44
+ return default unless $stdout.isatty
45
+
46
+ # clear the buffer
47
+ if ARGV&.length
48
+ ARGV.length.times do
49
+ ARGV.shift
50
+ end
51
+ end
52
+ system 'stty cbreak'
53
+
54
+ cw = white
55
+ cbw = boldwhite
56
+ cbg = boldgreen
57
+ cd = Color.default
58
+
59
+ options = unless default.nil?
60
+ "#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
61
+ else
62
+ "#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
63
+ end
64
+ $stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
65
+ res = $stdin.sysread 1
66
+ puts
67
+ system 'stty cooked'
68
+
69
+ res.chomp!
70
+ res.downcase!
71
+
72
+ return default if res.empty?
73
+
74
+ res =~ /y/i ? true : false
75
+ end
76
+
77
+ def fzf
78
+ @fzf ||= install_fzf
79
+ end
80
+
81
+ def install_fzf
82
+ fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
83
+ FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
84
+ fzf_bin = File.join(fzf_dir, 'bin/fzf')
85
+ return fzf_bin if File.exist?(fzf_bin)
86
+
87
+ prev_level = Doing.logger.level
88
+ Doing.logger.adjust_verbosity({ log_level: :info })
89
+ Doing.logger.log_now(:warn, 'Compiling and installing fzf -- this will only happen once')
90
+ Doing.logger.log_now(:warn, 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
91
+
92
+ system("'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
93
+ unless File.exist?(fzf_bin)
94
+ Doing.logger.log_now(:warn, 'Error installing, trying again as root')
95
+ system("sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
96
+ end
97
+ raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues') unless File.exist?(fzf_bin)
98
+
99
+ Doing.logger.info("fzf installed to #{fzf}")
100
+ Doing.logger.adjust_verbosity({ log_level: prev_level })
101
+ fzf_bin
102
+ end
103
+
104
+ ##
105
+ ## Generate a menu of options and allow user selection
106
+ ##
107
+ ## @return [String] The selected option
108
+ ##
109
+ def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
110
+ return nil unless $stdout.isatty
111
+
112
+ # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
113
+ fzf_args << %(--prompt "#{prompt}")
114
+ fzf_args << '--multi' if multiple
115
+ header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
116
+ fzf_args << %(--header "#{header}")
117
+ options.sort! if sorted
118
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
119
+ return false if res.strip.size.zero?
120
+
121
+ res
122
+ end
123
+
124
+ ##
125
+ ## Create an interactive menu to select from a set of Items
126
+ ##
127
+ ## @param items [Array] list of items
128
+ ## @param opt [Hash] options
129
+ ## @param include_section [Boolean] include section
130
+ ##
131
+ ## @option opt [String] :header
132
+ ## @option opt [String] :prompt
133
+ ## @option opt [String] :query
134
+ ## @option opt [Boolean] :show_if_single
135
+ ## @option opt [Boolean] :menu
136
+ ## @option opt [Boolean] :sort
137
+ ## @option opt [Boolean] :multiple
138
+ ## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
139
+ ##
140
+ def choose_from_items(items, **opt)
141
+ return items unless $stdout.isatty
142
+
143
+ return nil unless items.count.positive?
144
+
145
+ case_sensitive = opt.fetch(:case, :smart).normalize_case
146
+ header = opt.fetch(:header, 'Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit')
147
+ prompt = opt.fetch(:prompt, 'Select entries to act on > ')
148
+ query = opt.fetch(:query) { opt.fetch(:search, '') }
149
+ include_section = opt.fetch(:include_section, false)
150
+
151
+ pad = items.length.to_s.length
152
+ options = items.map.with_index do |item, i|
153
+ out = [
154
+ format("%#{pad}d", i),
155
+ ') ',
156
+ format('%13s', item.date.relative_date),
157
+ ' | ',
158
+ item.title
159
+ ]
160
+ if include_section
161
+ out.concat([
162
+ ' (',
163
+ item.section,
164
+ ') '
165
+ ])
166
+ end
167
+ out.join('')
168
+ end
169
+
170
+ fzf_args = [
171
+ %(--header="#{header}"),
172
+ %(--prompt="#{prompt.sub(/ *$/, ' ')}"),
173
+ opt.fetch(:multiple) ? '--multi' : '--no-multi',
174
+ '-0',
175
+ '--bind ctrl-a:select-all',
176
+ %(-q "#{query}"),
177
+ '--info=inline'
178
+ ]
179
+ fzf_args.push('-1') unless opt.fetch(:show_if_single)
180
+ fzf_args << case case_sensitive
181
+ when :sensitive
182
+ '+i'
183
+ when :ignore
184
+ '-i'
185
+ end
186
+ fzf_args << '-e' if opt.fetch(:exact, false)
187
+
188
+
189
+ unless opt.fetch(:menu)
190
+ raise InvalidArgument, "Can't skip menu when no query is provided" unless query && !query.empty?
191
+
192
+ fzf_args.concat([%(--filter="#{query}"), opt.fetch(:sort) ? '' : '--no-sort'])
193
+ end
194
+
195
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
196
+ selected = []
197
+ res.split(/\n/).each do |item|
198
+ idx = item.match(/^ *(\d+)\)/)[1].to_i
199
+ selected.push(items[idx])
200
+ end
201
+
202
+ opt.fetch(:multiple) ? selected : selected[0]
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Section Object
5
+ class Section
6
+ attr_accessor :original, :title
7
+
8
+ def initialize(title, original: nil)
9
+ super()
10
+
11
+ @title = title
12
+
13
+ @original = if original.nil?
14
+ "#{title}:"
15
+ else
16
+ original =~ /:(\s+@\S+(\(.*?\))?)*$/ ? original : "#{original}:"
17
+ end
18
+ end
19
+
20
+ # Outputs section title
21
+ def to_s
22
+ @title
23
+ end
24
+
25
+ # @private
26
+ def inspect
27
+ %(#<Doing::Section @title="#{@title}" @original="#{@original}">)
28
+ end
29
+ end
30
+ end
data/lib/doing/string.rb CHANGED
@@ -20,8 +20,7 @@ module Doing
20
20
  ## can be separated by up to *distance* characters in
21
21
  ## haystack, spaces indicate unlimited distance.
22
22
  ##
23
- ## @example "this word".to_rx(2) =>
24
- ## /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/
23
+ ## @example `"this word".to_rx(2) => /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/`
25
24
  ##
26
25
  ## @param distance [Integer] Allowed distance
27
26
  ## between characters
@@ -63,6 +62,15 @@ module Doing
63
62
  end
64
63
  end
65
64
 
65
+ # Compress multiple spaces to single space
66
+ def compress
67
+ gsub(/ +/, ' ').strip
68
+ end
69
+
70
+ def compress!
71
+ replace compress
72
+ end
73
+
66
74
  ## @param (see #highlight_tags)
67
75
  def highlight_tags!(color = 'yellow')
68
76
  replace highlight_tags(color)
@@ -289,11 +297,29 @@ module Doing
289
297
  title
290
298
  end
291
299
 
292
- def tag!(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false)
293
- replace tag(tag, value: value, remove: remove, rename_to: rename_to, regex: regex, single: single)
300
+ ##
301
+ ## Add, rename, or remove a tag in place
302
+ ##
303
+ ## @see #tag
304
+ ##
305
+ def tag!(tag, **options)
306
+ replace tag(tag, **options)
294
307
  end
295
308
 
296
- def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false)
309
+ ##
310
+ ## Add, rename, or remove a tag
311
+ ##
312
+ ## @param tag The tag
313
+ ## @param value [String] Value for tag (@tag(value))
314
+ ## @param remove [Boolean] Remove the tag instead of adding
315
+ ## @param rename_to [String] Replace tag with this tag
316
+ ## @param regex [Boolean] Tag is regular expression
317
+ ## @param single [Boolean] Operating on a single item (for logging)
318
+ ## @param force [Boolean] With rename_to, add tag if it doesn't exist
319
+ ##
320
+ ## @return [String] The string with modified tags
321
+ ##
322
+ def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: false)
297
323
  log_level = single ? :info : :debug
298
324
  title = dup
299
325
  title.chomp!
@@ -307,13 +333,14 @@ module Doing
307
333
  end
308
334
 
309
335
  if remove || rename_to
310
- return title unless title =~ /#{rx_tag}(?=[ (]|$)/
336
+ rx = Regexp.new("(?<=^| )@#{rx_tag}(?<parens>\\((?<value>[^)]*)\\))?(?= |$)", case_sensitive)
337
+ m = title.match(rx)
311
338
 
312
- rx = Regexp.new("(^| )@#{rx_tag}(\\([^)]*\\))?(?= |$)", case_sensitive)
313
- if title =~ rx
339
+ if m.nil? && rename_to && force
340
+ title.tag!(rename_to, value: value, single: single)
341
+ elsif m
314
342
  title.gsub!(rx) do
315
- m = Regexp.last_match
316
- rename_to ? "#{m[1]}@#{rename_to}#{m[2]}" : m[1]
343
+ rename_to ? "@#{rename_to}#{value.nil? ? m['parens'] : "(#{value})"}" : ''
317
344
  end
318
345
 
319
346
  title.dedup_tags!
@@ -373,9 +400,16 @@ module Doing
373
400
  title
374
401
  end
375
402
 
376
- # Returns the last escape sequence from a string
403
+ # Returns the last escape sequence from a string.
377
404
  #
378
- # @param string The string to examine
405
+ # Actually returns all escape codes, with the assumption
406
+ # that the result of inserting them will generate the
407
+ # same color as was set at the end of the string.
408
+ # Because you can send modifiers like dark and bold
409
+ # separate from color codes, only using the last code
410
+ # may not render the same style.
411
+ #
412
+ # @return [String] All escape codes in string
379
413
  #
380
414
  def last_color
381
415
  scan(/\e\[[\d;]+m/).join('')
@@ -386,17 +420,20 @@ module Doing
386
420
  ##
387
421
  ## @param opt [Hash] Additional Options
388
422
  ##
389
- def link_urls!(opt = {})
390
- replace link_urls(opt)
423
+ def link_urls!(**opt)
424
+ fmt = opt.fetch(:format, :html)
425
+ replace link_urls(format: fmt)
391
426
  end
392
427
 
393
- def link_urls(opt = {})
394
- opt[:format] ||= :html
428
+ def link_urls(**opt)
429
+ fmt = opt.fetch(:format, :html)
430
+ return self unless fmt
431
+
395
432
  str = dup
396
433
 
397
- str = str.remove_self_links if opt[:format] == :markdown
434
+ str = str.remove_self_links if fmt == :markdown
398
435
 
399
- str.replace_qualified_urls(format: opt[:format]).clean_unlinked_urls
436
+ str.replace_qualified_urls(format: fmt).clean_unlinked_urls
400
437
  end
401
438
 
402
439
  # Remove <self-linked> formatting
@@ -412,21 +449,24 @@ module Doing
412
449
  end
413
450
 
414
451
  # Replace qualified urls
415
- def replace_qualified_urls(opt = {})
416
- opt[:format] ||= :html
452
+ def replace_qualified_urls(**options)
453
+ fmt = options.fetch(:format, :html)
417
454
  gsub(%r{(?mi)(?x:
418
455
  (?<!["'\[(\\])
419
- ((http|https)://)
420
- ([\w\-]+(\.[\w\-]+)+)
421
- ([\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
456
+ (?<protocol>(?:http|https)://)
457
+ (?<domain>[\w\-]+(?:\.[\w\-]+)+)
458
+ (?<path>[\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
422
459
  )}) do |_match|
423
460
  m = Regexp.last_match
424
- proto = m[1].nil? ? 'http://' : ''
425
- case opt[:format]
461
+ url = "#{m['domain']}#{m['path']}"
462
+ proto = m['protocol'].nil? ? 'http://' : m['protocol']
463
+ case fmt
464
+ when :terminal
465
+ TTY::Link.link_to("#{proto}#{url}", "#{proto}#{url}")
426
466
  when :html
427
- %(<a href="#{proto}#{m[0]}" title="Link to #{m[0].sub(%r{^https?://}, '')}">[#{m[3]}]</a>)
467
+ %(<a href="#{proto}#{url}" title="Link to #{m['domain']}">[#{url}]</a>)
428
468
  when :markdown
429
- "[#{m[0]}](#{proto}#{m[0]})"
469
+ "[#{url}](#{proto}#{url})"
430
470
  else
431
471
  m[0]
432
472
  end
@@ -444,5 +484,41 @@ module Doing
444
484
  end
445
485
  end
446
486
  end
487
+
488
+ def set_type(kind = nil)
489
+ if kind
490
+ case kind.to_s
491
+ when /^a/i
492
+ gsub(/^\[ *| *\]$/, '').split(/ *, */)
493
+ when /^i/i
494
+ to_i
495
+ when /^f/i
496
+ to_f
497
+ when /^sy/i
498
+ sub(/^:/, '').to_sym
499
+ when /^b/i
500
+ self =~ /^(true|yes)$/ ? true : false
501
+ else
502
+ to_s
503
+ end
504
+ else
505
+ case self
506
+ when / *, */
507
+ gsub(/^\[ *| *\]$/, '').split(/ *, */)
508
+ when /^[0-9]+$/
509
+ to_i
510
+ when /^[0-9]+\.[0-9]+$/
511
+ to_f
512
+ when /^:\w+/
513
+ sub(/^:/, '').to_sym
514
+ when /^(true|yes)$/i
515
+ true
516
+ when /^(false|no)$/i
517
+ false
518
+ else
519
+ to_s
520
+ end
521
+ end
522
+ end
447
523
  end
448
524
  end
data/lib/doing/util.rb CHANGED
@@ -21,11 +21,17 @@ module Doing
21
21
  def exec_available(cli)
22
22
  return false if cli.nil?
23
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
24
+ !TTY::Which.which(cli).nil?
25
+ end
26
+
27
+ ##
28
+ ## Return the first valid executable from a list of commands
29
+ ##
30
+ ## @example `Doing::Util.first_available_exec('bat', 'less -Xr', 'more -r', 'cat')`
31
+ ##
32
+ def first_available_exec(*commands)
33
+ commands.compact.map(&:strip).reject(&:empty?).uniq
34
+ .find { |cmd| exec_available(cmd.split.first) }
29
35
  end
30
36
 
31
37
  def merge_default_proc(target, overwrite)
@@ -116,6 +122,7 @@ module Doing
116
122
 
117
123
  File.open(file, 'w+') do |f|
118
124
  f.puts content
125
+ Doing.logger.debug('Write:', "File written: #{file}")
119
126
  end
120
127
 
121
128
  Hooks.trigger :post_write, file
@@ -183,7 +190,8 @@ module Doing
183
190
  Doing.logger.debug('ENV:', 'No EDITOR environment variable, testing available editors')
184
191
  editors = %w[vim vi code subl mate mvim nano emacs]
185
192
  editors.each do |ed|
186
- return ed if exec_available(ed)
193
+ return TTY::Which.which(ed) if TTY::Which.which(ed)
194
+
187
195
  Doing.logger.debug('ENV:', "#{ed} not available")
188
196
  end
189
197
 
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.0.25'
2
+ VERSION = '2.1.0pre'
3
3
  end