doing 2.0.25 → 2.1.0pre

Sign up to get free protection for your applications and to get access to all the features.
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