doing 2.1.40 → 2.1.41

Sign up to get free protection for your applications and to get access to all the features.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +4 -4
  6. data/bin/commands/changes.rb +1 -1
  7. data/bin/commands/tag_dir.rb +49 -15
  8. data/{Dockerfile → docker/Dockerfile} +3 -1
  9. data/{Dockerfile-2.6 → docker/Dockerfile-2.6} +2 -2
  10. data/{Dockerfile-2.7 → docker/Dockerfile-2.7} +2 -2
  11. data/{Dockerfile-3.0 → docker/Dockerfile-3.0} +2 -2
  12. data/{bash_profile → docker/bash_profile} +0 -0
  13. data/{inputrc → docker/inputrc} +0 -0
  14. data/docs/doc/Array.html +84 -2
  15. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  16. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  17. data/docs/doc/BooleanTermParser/Query.html +1 -1
  18. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  19. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  20. data/docs/doc/BooleanTermParser.html +1 -1
  21. data/docs/doc/Doing/ArrayNestedHash.html +198 -0
  22. data/docs/doc/Doing/ArrayTags.html +424 -0
  23. data/docs/doc/Doing/CSVExport.html +266 -0
  24. data/docs/doc/Doing/CalendarImport.html +232 -0
  25. data/docs/doc/Doing/Change.html +617 -0
  26. data/docs/doc/Doing/Changes.html +468 -0
  27. data/docs/doc/Doing/ChronifyArray.html +347 -0
  28. data/docs/doc/Doing/ChronifyNumeric.html +271 -0
  29. data/docs/doc/Doing/ChronifyString.html +682 -0
  30. data/docs/doc/Doing/Color.html +2 -2
  31. data/docs/doc/Doing/Completion/BashCompletions.html +445 -0
  32. data/docs/doc/Doing/Completion/FishCompletions.html +445 -0
  33. data/docs/doc/Doing/Completion/StringUtils.html +229 -0
  34. data/docs/doc/Doing/Completion/ZshCompletions.html +445 -0
  35. data/docs/doc/Doing/Completion.html +17 -3
  36. data/docs/doc/Doing/Configuration.html +1 -1
  37. data/docs/doc/Doing/DayOneRenderer.html +383 -0
  38. data/docs/doc/Doing/DayoneExport.html +290 -0
  39. data/docs/doc/Doing/DoingImport.html +391 -0
  40. data/docs/doc/Doing/Entry.html +381 -0
  41. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  42. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  43. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  44. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  45. data/docs/doc/Doing/Errors/HistoryLimitError.html +1 -1
  46. data/docs/doc/Doing/Errors/InvalidPlugin.html +1 -1
  47. data/docs/doc/Doing/Errors/MissingBackupFile.html +1 -1
  48. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  49. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  50. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  51. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  52. data/docs/doc/Doing/Errors.html +1 -1
  53. data/docs/doc/Doing/HTMLExport.html +256 -0
  54. data/docs/doc/Doing/Hooks.html +1 -1
  55. data/docs/doc/Doing/Item.html +47 -3
  56. data/docs/doc/Doing/ItemDates.html +564 -0
  57. data/docs/doc/Doing/ItemQuery.html +614 -0
  58. data/docs/doc/Doing/ItemState.html +387 -0
  59. data/docs/doc/Doing/ItemTags.html +498 -0
  60. data/docs/doc/Doing/Items.html +460 -11
  61. data/docs/doc/Doing/JSONExport.html +222 -0
  62. data/docs/doc/Doing/Logger.html +1 -1
  63. data/docs/doc/Doing/MarkdownExport.html +266 -0
  64. data/docs/doc/Doing/MarkdownRenderer.html +383 -0
  65. data/docs/doc/Doing/Note.html +16 -3
  66. data/docs/doc/Doing/Pager.html +1 -1
  67. data/docs/doc/Doing/Plugins.html +1 -1
  68. data/docs/doc/Doing/Prompt.html +31 -682
  69. data/docs/doc/Doing/PromptChoose.html +484 -0
  70. data/docs/doc/Doing/PromptFZF.html +391 -0
  71. data/docs/doc/Doing/PromptInput.html +572 -0
  72. data/docs/doc/Doing/PromptSTD.html +293 -0
  73. data/docs/doc/Doing/PromptYN.html +237 -0
  74. data/docs/doc/Doing/Section.html +58 -2
  75. data/docs/doc/Doing/StringHighlight.html +533 -0
  76. data/docs/doc/Doing/StringNormalize.html +929 -0
  77. data/docs/doc/Doing/StringQuery.html +725 -0
  78. data/docs/doc/Doing/StringTags.html +884 -0
  79. data/docs/doc/Doing/StringTransform.html +565 -0
  80. data/docs/doc/Doing/StringTruncate.html +448 -0
  81. data/docs/doc/Doing/StringURL.html +409 -0
  82. data/docs/doc/Doing/SymbolNormalize.html +341 -0
  83. data/docs/doc/Doing/TaskPaperExport.html +222 -0
  84. data/docs/doc/Doing/TemplateExport.html +249 -0
  85. data/docs/doc/Doing/TemplateString.html +101 -2
  86. data/docs/doc/Doing/TimingImport.html +285 -0
  87. data/docs/doc/Doing/Types.html +1 -1
  88. data/docs/doc/Doing/Util/Backup.html +9 -7
  89. data/docs/doc/Doing/Util.html +2 -2
  90. data/docs/doc/Doing/Version.html +523 -0
  91. data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
  92. data/docs/doc/Doing/WWID.html +4377 -217
  93. data/docs/doc/Doing/WWIDDisplay.html +865 -0
  94. data/docs/doc/Doing/WWIDEditor.html +466 -0
  95. data/docs/doc/Doing/WWIDFileTools.html +359 -0
  96. data/docs/doc/Doing/WWIDFilter.html +466 -0
  97. data/docs/doc/Doing/WWIDGuess.html +299 -0
  98. data/docs/doc/Doing/WWIDInteractive.html +752 -0
  99. data/docs/doc/Doing/WWIDModify.html +1078 -0
  100. data/docs/doc/Doing/WWIDTags.html +302 -0
  101. data/docs/doc/Doing/WWIDTimers.html +359 -0
  102. data/docs/doc/Doing/WWIDUtil.html +510 -0
  103. data/docs/doc/Doing.html +9 -6
  104. data/docs/doc/FalseClass.html +1 -1
  105. data/docs/doc/GLI/Commands/Help.html +1 -1
  106. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  107. data/docs/doc/GLI/Commands.html +1 -1
  108. data/docs/doc/GLI.html +1 -1
  109. data/docs/doc/Hash.html +1 -1
  110. data/docs/doc/Numeric.html +23 -78
  111. data/docs/doc/Object.html +1 -1
  112. data/docs/doc/PhraseParser/Operator.html +1 -1
  113. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  114. data/docs/doc/PhraseParser/Query.html +1 -1
  115. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  116. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  117. data/docs/doc/PhraseParser/TermClause.html +1 -1
  118. data/docs/doc/PhraseParser.html +1 -1
  119. data/docs/doc/Status.html +1 -1
  120. data/docs/doc/String.html +58 -633
  121. data/docs/doc/Symbol.html +9 -224
  122. data/docs/doc/Time.html +119 -13
  123. data/docs/doc/TrueClass.html +1 -1
  124. data/docs/doc/_index.html +324 -8
  125. data/docs/doc/class_list.html +1 -1
  126. data/docs/doc/file.README.html +1 -1
  127. data/docs/doc/index.html +1 -1
  128. data/docs/doc/method_list.html +2326 -542
  129. data/docs/doc/top-level-namespace.html +2 -2
  130. data/doing.rdoc +13 -3
  131. data/lib/completion/_doing.zsh +1 -1
  132. data/lib/completion/doing.bash +2 -2
  133. data/lib/completion/doing.fish +3 -1
  134. data/lib/doing/array/array.rb +16 -12
  135. data/lib/doing/array/nested_hash.rb +1 -1
  136. data/lib/doing/array/tags.rb +6 -5
  137. data/lib/doing/changelog/changelog.rb +6 -0
  138. data/lib/doing/chronify/array.rb +1 -3
  139. data/lib/doing/chronify/chronify.rb +12 -0
  140. data/lib/doing/chronify/numeric.rb +3 -2
  141. data/lib/doing/chronify/string.rb +1 -1
  142. data/lib/doing/completion/completion_string.rb +25 -0
  143. data/lib/doing/completion.rb +1 -1
  144. data/lib/doing/good.rb +8 -0
  145. data/lib/doing/item/dates.rb +1 -1
  146. data/lib/doing/{item.rb → item/item.rb} +10 -5
  147. data/lib/doing/item/query.rb +1 -1
  148. data/lib/doing/item/state.rb +1 -1
  149. data/lib/doing/item/tags.rb +1 -1
  150. data/lib/doing/items/filter.rb +67 -0
  151. data/lib/doing/items/items.rb +57 -0
  152. data/lib/doing/items/modify.rb +36 -0
  153. data/lib/doing/items/sections.rb +83 -0
  154. data/lib/doing/items/util.rb +74 -0
  155. data/lib/doing/normalize.rb +10 -2
  156. data/lib/doing/plugins/export/markdown_export.rb +4 -2
  157. data/lib/doing/plugins/import/doing_import.rb +1 -1
  158. data/lib/doing/prompt/choose.rb +118 -0
  159. data/lib/doing/prompt/fzf.rb +84 -0
  160. data/lib/doing/prompt/input.rb +129 -0
  161. data/lib/doing/prompt/prompt.rb +41 -0
  162. data/lib/doing/prompt/std.rb +32 -0
  163. data/lib/doing/prompt/yn.rb +64 -0
  164. data/lib/doing/section.rb +4 -0
  165. data/lib/doing/string/highlight.rb +1 -1
  166. data/lib/doing/string/query.rb +1 -1
  167. data/lib/doing/string/string.rb +18 -7
  168. data/lib/doing/string/tags.rb +14 -3
  169. data/lib/doing/string/transform.rb +1 -1
  170. data/lib/doing/string/truncate.rb +1 -1
  171. data/lib/doing/string/url.rb +1 -1
  172. data/lib/doing/time.rb +19 -1
  173. data/lib/doing/util_backup.rb +2 -2
  174. data/lib/doing/version.rb +1 -1
  175. data/lib/doing/wwid/display.rb +357 -360
  176. data/lib/doing/wwid/editor.rb +173 -176
  177. data/lib/doing/wwid/filetools.rb +156 -159
  178. data/lib/doing/wwid/filter.rb +191 -183
  179. data/lib/doing/wwid/guess.rb +58 -60
  180. data/lib/doing/wwid/interactive.rb +332 -330
  181. data/lib/doing/wwid/modify.rb +509 -512
  182. data/lib/doing/wwid/tags.rb +38 -41
  183. data/lib/doing/wwid/timers.rb +293 -296
  184. data/lib/doing/{wwid.rb → wwid/wwid.rb} +32 -23
  185. data/lib/doing/wwid/wwidutil.rb +79 -82
  186. data/lib/doing.rb +5 -5
  187. data/lib/helpers/threaded_tests.rb +1 -0
  188. metadata +76 -14
  189. data/lib/doing/changelog.rb +0 -6
  190. data/lib/doing/completion/string.rb +0 -17
  191. data/lib/doing/items.rb +0 -221
  192. data/lib/doing/prompt.rb +0 -330
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ class Items < Array
5
+ # # Create a deep copy of Items
6
+ # def clone
7
+ # Marshal.load(Marshal.dump(self))
8
+ # end
9
+
10
+ def delete(item)
11
+ deleted = nil
12
+ each_with_index do |i, idx|
13
+ if i.equal?(item, match_section: true)
14
+ deleted = delete_at(idx)
15
+ break
16
+ end
17
+ end
18
+ deleted
19
+ end
20
+
21
+ ##
22
+ ## Get all tags on Items in self
23
+ ##
24
+ ## @return [Array] array of tags
25
+ ##
26
+ def all_tags
27
+ each_with_object([]) do |entry, tags|
28
+ tags.concat(entry.tags).sort!.uniq!
29
+ end
30
+ end
31
+
32
+ ##
33
+ ## Return Items containing items that don't exist in
34
+ ## receiver
35
+ ##
36
+ ## @param items [Items] Receiver
37
+ ##
38
+ ## @return [Hash] Hash of added and deleted items
39
+ ##
40
+ def diff(items)
41
+ a = clone
42
+ b = items.clone
43
+
44
+ a.delete_if do |item|
45
+ if b.include?(item)
46
+ b.delete(item)
47
+ true
48
+ else
49
+ false
50
+ end
51
+ end
52
+ { added: b, deleted: a }
53
+ end
54
+
55
+ ##
56
+ ## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
57
+ ##
58
+ ## @return [Items] Items array with duplicate entries removed
59
+ ##
60
+ def dedup(match_section: true)
61
+ unique = Items.new
62
+ each do |item|
63
+ unique.push(item) unless unique.include?(item, match_section: match_section)
64
+ end
65
+
66
+ unique
67
+ end
68
+
69
+ # @see #dedup
70
+ def dedup!(match_section: true)
71
+ replace dedup(match_section: match_section)
72
+ end
73
+ end
74
+ end
@@ -4,7 +4,7 @@ module Doing
4
4
  ##
5
5
  ## String to symbol conversion
6
6
  ##
7
- class ::String
7
+ module StringNormalize
8
8
  ##
9
9
  ## Convert tag sort string to a qualified type
10
10
  ##
@@ -160,7 +160,7 @@ module Doing
160
160
  ##
161
161
  ## Symbol helpers
162
162
  ##
163
- class ::Symbol
163
+ module SymbolNormalize
164
164
  def normalize_tag_sort(default = :name)
165
165
  to_s.normalize_tag_sort
166
166
  end
@@ -186,3 +186,11 @@ module Doing
186
186
  end
187
187
  end
188
188
  end
189
+
190
+ class ::String
191
+ include Doing::StringNormalize
192
+ end
193
+
194
+ class ::Symbol
195
+ include Doing::SymbolNormalize
196
+ end
@@ -5,6 +5,7 @@
5
5
  # author: Brett Terpstra
6
6
  # url: https://brettterpstra.com
7
7
  module Doing
8
+ # @private
8
9
  class MarkdownRenderer
9
10
  attr_accessor :items, :page_title, :totals
10
11
 
@@ -15,12 +16,13 @@ module Doing
15
16
  end
16
17
 
17
18
  def get_binding
18
- binding()
19
+ binding
19
20
  end
20
21
  end
21
22
 
23
+ # Markdown Export Plugin
22
24
  class MarkdownExport
23
- include Doing::Util
25
+ include Util
24
26
 
25
27
  def self.settings
26
28
  {
@@ -125,7 +125,7 @@ module Doing
125
125
  next if line =~ /^\s*$/
126
126
 
127
127
  case line
128
- when /^(\S[\S ]+):\s*(@\S+\s*)*$/
128
+ when /^(\S[\S ]+):(\s+@[\w\-_.]+(?= |$))*\s*$/
129
129
  section = Regexp.last_match(1)
130
130
  current = 0
131
131
  when /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Methods for creating interactive menus of options and items
5
+ module PromptChoose
6
+ ##
7
+ ## Generate a menu of options and allow user selection
8
+ ##
9
+ ## @return [String] The selected option
10
+ ##
11
+ ## @param options [Array] The options from which to choose
12
+ ## @param prompt [String] The prompt
13
+ ## @param multiple [Boolean] If true, allow multiple selections
14
+ ## @param sorted [Boolean] If true, sort selections alphanumerically
15
+ ## @param fzf_args [Array] Additional fzf arguments
16
+ ##
17
+ def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
18
+ return nil unless $stdout.isatty
19
+
20
+ # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
21
+ default_args = []
22
+ default_args << %(--prompt="#{prompt}")
23
+ default_args << "--height=#{options.count + 2}"
24
+ default_args << '--info=inline'
25
+ default_args << '--multi' if multiple
26
+ header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
27
+ default_args << %(--header="#{header}")
28
+ default_args.concat(fzf_args)
29
+ options.sort! if sorted
30
+
31
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{default_args.join(' ')}`
32
+ return false if res.strip.size.zero?
33
+
34
+ res
35
+ end
36
+
37
+ ##
38
+ ## Create an interactive menu to select from a set of Items
39
+ ##
40
+ ## @param items [Array] list of items
41
+ ## @param opt Additional options
42
+ ##
43
+ ## @option opt [Boolean] :include_section Include section name for each item in menu
44
+ ## @option opt [String] :header A custom header string
45
+ ## @option opt [String] :prompt A custom prompt string
46
+ ## @option opt [String] :query Initial query
47
+ ## @option opt [Boolean] :show_if_single Show menu even if there's only one option
48
+ ## @option opt [Boolean] :menu Show menu
49
+ ## @option opt [Boolean] :sort Sort options
50
+ ## @option opt [Boolean] :multiple Allow multiple selections
51
+ ## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
52
+ ##
53
+ def choose_from_items(items, **opt)
54
+ return items unless $stdout.isatty
55
+
56
+ return nil unless items.count.positive?
57
+
58
+ case_sensitive = opt.fetch(:case, :smart).normalize_case
59
+ header = opt.fetch(:header, 'Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit')
60
+ prompt = opt.fetch(:prompt, 'Select entries to act on > ')
61
+ query = opt.fetch(:query) { opt.fetch(:search, '') }
62
+ include_section = opt.fetch(:include_section, false)
63
+
64
+ pad = items.length.to_s.length
65
+ options = items.map.with_index do |item, i|
66
+ out = [
67
+ format("%#{pad}d", i),
68
+ ') ',
69
+ format('%16s', item.date.strftime('%Y-%m-%d %H:%M')),
70
+ ' | ',
71
+ item.title
72
+ ]
73
+ if include_section
74
+ out.concat([
75
+ ' (',
76
+ item.section,
77
+ ') '
78
+ ])
79
+ end
80
+ out.join('')
81
+ end
82
+
83
+ fzf_args = [
84
+ %(--header="#{header}"),
85
+ %(--prompt="#{prompt.sub(/ *$/, ' ')}"),
86
+ opt.fetch(:multiple) ? '--multi' : '--no-multi',
87
+ '-0',
88
+ '--bind ctrl-a:select-all',
89
+ %(-q "#{query}"),
90
+ '--info=inline'
91
+ ]
92
+ fzf_args.push('-1') unless opt.fetch(:show_if_single, false)
93
+ fzf_args << case case_sensitive
94
+ when :sensitive
95
+ '+i'
96
+ when :ignore
97
+ '-i'
98
+ end
99
+ fzf_args << '-e' if opt.fetch(:exact, false)
100
+
101
+
102
+ unless opt.fetch(:menu)
103
+ raise InvalidArgument, "Can't skip menu when no query is provided" unless query && !query.empty?
104
+
105
+ fzf_args.concat([%(--filter="#{query}"), opt.fetch(:sort) ? '' : '--no-sort'])
106
+ end
107
+
108
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
109
+ selected = []
110
+ res.split(/\n/).each do |item|
111
+ idx = item.match(/^ *(\d+)\)/)[1].to_i
112
+ selected.push(items[idx])
113
+ end
114
+
115
+ opt.fetch(:multiple) ? selected : selected[0]
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Methods for working installing/using FuzzyFileFinder
5
+ module PromptFZF
6
+ ##
7
+ ## Get path to fzf binary, installing if needed
8
+ ##
9
+ ## @return [String] Path to fzf binary
10
+ ##
11
+ def fzf
12
+ @fzf ||= install_fzf
13
+ end
14
+
15
+ ##
16
+ ## Remove fzf binary
17
+ ##
18
+ def uninstall_fzf
19
+ fzf_bin = File.join(File.dirname(__FILE__), '../../helpers/fzf/bin/fzf')
20
+ FileUtils.rm_f(fzf_bin) if File.exist?(fzf_bin)
21
+ Doing.logger.warn('fzf:', "removed #{fzf_bin}")
22
+ end
23
+
24
+ ##
25
+ ## Return the path to the fzf binary
26
+ ##
27
+ ## @return [String] Path to fzf
28
+ ##
29
+ def which_fzf
30
+ fzf_dir = File.join(File.dirname(__FILE__), '../../helpers/fzf')
31
+ fzf_bin = File.join(fzf_dir, 'bin/fzf')
32
+ return fzf_bin if File.exist?(fzf_bin)
33
+
34
+ Doing.logger.debug('fzf:', 'Using user-installed fzf')
35
+ TTY::Which.which('fzf')
36
+ end
37
+
38
+ ##
39
+ ## Install fzf on the current system. Installs to a
40
+ ## subdirectory of the gem
41
+ ##
42
+ ## @param force [Boolean] If true, reinstall if
43
+ ## needed
44
+ ##
45
+ ## @return [String] Path to fzf binary
46
+ ##
47
+ def install_fzf(force: false)
48
+ if force
49
+ uninstall_fzf
50
+ elsif which_fzf
51
+ return which_fzf
52
+ end
53
+
54
+ fzf_dir = File.join(File.dirname(__FILE__), '../../helpers/fzf')
55
+ FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
56
+ fzf_bin = File.join(fzf_dir, 'bin/fzf')
57
+ return fzf_bin if File.exist?(fzf_bin)
58
+
59
+ prev_level = Doing.logger.level
60
+ Doing.logger.adjust_verbosity({ log_level: :info })
61
+ Doing.logger.log_now(:warn, 'fzf:', 'Compiling and installing fzf -- this will only happen once')
62
+ Doing.logger.log_now(:warn, 'fzf:', 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
63
+
64
+ silence_std
65
+ `'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null`
66
+ unless File.exist?(fzf_bin)
67
+ restore_std
68
+ Doing.logger.log_now(:warn, 'Error installing, trying again as root')
69
+ silence_std
70
+ `sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null`
71
+ end
72
+ restore_std
73
+ unless File.exist?(fzf_bin)
74
+ Doing.logger.error('fzf:', 'unable to install fzf. You can install manually and Doing will use the system version.')
75
+ Doing.logger.error('fzf:', 'see https://github.com/junegunn/fzf#installation')
76
+ raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues')
77
+ end
78
+
79
+ Doing.logger.info('fzf:', "installed to #{fzf}")
80
+ Doing.logger.adjust_verbosity({ log_level: prev_level })
81
+ fzf_bin
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Methods for requesting user text input
5
+ module PromptInput
6
+ ##
7
+ ## Request single-line input
8
+ ##
9
+ ## @param prompt [String] The prompt
10
+ ## @param default_response [String] The default
11
+ ## response returned if
12
+ ## :default_answer is
13
+ ## true
14
+ ##
15
+ ## @return [String] The user response
16
+ ##
17
+ ## @deprecated Use {#read_line} instead
18
+ ##
19
+ def enter_text(prompt, default_response: '')
20
+ $stdin.reopen('/dev/tty')
21
+ return default_response if @default_answer
22
+
23
+ print "#{yellow(prompt).sub(/:?$/, ':')} #{reset}"
24
+ $stdin.gets.strip
25
+ end
26
+
27
+ ##
28
+ ## Request single-line input using Readline. Allows
29
+ ## for control sequences and tab completions
30
+ ##
31
+ ## @param prompt [String] The prompt
32
+ ## @param completions [Array] Array of tab
33
+ ## completions
34
+ ## @param default_response [String] The default
35
+ ## response returned if
36
+ ## :default_answer is
37
+ ## true
38
+ ##
39
+ ## @return [String] User input string
40
+ ##
41
+ def read_line(prompt: 'Enter text', completions: [], default_response: '')
42
+ $stdin.reopen('/dev/tty')
43
+ return default_response if @default_answer
44
+
45
+ unless completions.empty?
46
+ completions.sort!
47
+ comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
48
+ Readline.completion_append_character = ' '
49
+ Readline.completion_proc = comp
50
+ end
51
+
52
+ begin
53
+ Readline.readline("#{yellow(prompt).sub(/:?$/, ':')} #{reset}", true).strip
54
+ rescue Interrupt
55
+ raise UserCancelled
56
+ end
57
+ end
58
+
59
+ ##
60
+ ## Request multi-line input using Readline. Allows for
61
+ ## control sequences and tab completion
62
+ ##
63
+ ## @param prompt [String] The prompt
64
+ ## @param completions [Array] Array of tab
65
+ ## completions
66
+ ## @param default_response [String] The default
67
+ ## response returned if
68
+ ## :default_answer is
69
+ ## true
70
+ ##
71
+ ## @return [String] Multi-line result, joined with newlines
72
+ ##
73
+ def read_lines(prompt: 'Enter text', completions: [], default_response: '')
74
+ $stdin.reopen('/dev/tty')
75
+ return default_response if @default_answer
76
+
77
+ completions.sort!
78
+ comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
79
+ Readline.completion_append_character = ' '
80
+ Readline.completion_proc = comp
81
+ puts format(['%<promptcolor>s%<prompt>s %<textcolor>sEnter a blank line',
82
+ '(%<keycolor>sreturn twice%<textcolor>s)',
83
+ 'to end editing and save,',
84
+ '%<keycolor>sCTRL-C%<textcolor>s to cancel%<reset>s'].join(' '),
85
+ { promptcolor: boldgreen, prompt: prompt.sub(/:?$/, ':'),
86
+ textcolor: yellow, keycolor: boldwhite, reset: reset })
87
+
88
+ res = []
89
+
90
+ begin
91
+ while (line = Readline.readline('> ', true))
92
+ break if line.strip.empty?
93
+
94
+ res << line.chomp
95
+ end
96
+ rescue Interrupt
97
+ raise UserCancelled
98
+ end
99
+
100
+ res.join("\n").strip
101
+ end
102
+
103
+ ##
104
+ ## Request multi-line input
105
+ ##
106
+ ## @param prompt [String] The prompt
107
+ ## @param default_response [String] The default
108
+ ## response, returned if
109
+ ## :default_answer is
110
+ ## true
111
+ ##
112
+ ## @deprecated Use {#read_lines} instead
113
+ def request_lines(prompt: 'Enter text', default_response: '')
114
+ $stdin.reopen('/dev/tty')
115
+ return default_response if @default_answer
116
+
117
+ ask_note = []
118
+ reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
119
+ puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
120
+ loop do
121
+ res = reader.read_line(green('> '))
122
+ break if res.strip.empty?
123
+
124
+ ask_note.push(res)
125
+ end
126
+ ask_note.join("\n").strip
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'choose'
4
+ require_relative 'fzf'
5
+ require_relative 'input'
6
+ require_relative 'std'
7
+ require_relative 'yn'
8
+
9
+ module Doing
10
+ # Terminal Prompt methods
11
+ module Prompt
12
+ class << self
13
+ attr_writer :force_answer, :default_answer
14
+
15
+ include Color
16
+ include PromptSTD
17
+ include PromptInput
18
+ include PromptYN
19
+ include PromptFZF
20
+ include PromptChoose
21
+
22
+ ##
23
+ ## Value to return if prompt is skipped
24
+ ##
25
+ ## @return Force answer value
26
+ ##
27
+ def force_answer
28
+ @force_answer ||= nil
29
+ end
30
+
31
+ ##
32
+ ## If true, always return the default answer without prompting
33
+ ##
34
+ ## @return [Boolean] default answer
35
+ ##
36
+ def default_answer
37
+ @default_answer ||= false
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # STDOUT and STDERR methods
5
+ module PromptSTD
6
+ ##
7
+ ## Clear the terminal screen
8
+ ##
9
+ def clear_screen(msg = nil)
10
+ puts "\e[H\e[2J" if $stdout.tty?
11
+ puts msg if msg.good?
12
+ end
13
+
14
+ ##
15
+ ## Redirect STDOUT and STDERR to /dev/null or file
16
+ ##
17
+ ## @param file [String] a file path to redirect to
18
+ ##
19
+ def silence_std(file = '/dev/null')
20
+ $stdout = File.new(file, 'w')
21
+ $stderr = File.new(file, 'w')
22
+ end
23
+
24
+ ##
25
+ ## Restore silenced STDOUT and STDERR
26
+ ##
27
+ def restore_std
28
+ $stdout = STDOUT
29
+ $stderr = STDERR
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Request Yes/No answers on command line
5
+ module PromptYN
6
+ ##
7
+ ## Ask a yes or no question in the terminal
8
+ ##
9
+ ## @param question [String] The question
10
+ ## to ask
11
+ ## @param default_response [Boolean] default
12
+ ## response if no input
13
+ ##
14
+ ## @return [Boolean] yes or no
15
+ ##
16
+ def yn(question, default_response: false)
17
+ return @force_answer == :yes ? true : false unless @force_answer.nil?
18
+
19
+ $stdin.reopen('/dev/tty')
20
+
21
+ default = if default_response.is_a?(String)
22
+ default_response =~ /y/i ? true : false
23
+ else
24
+ default_response
25
+ end
26
+
27
+ # if global --default is set, answer default
28
+ return default if @default_answer
29
+
30
+ # if this isn't an interactive shell, answer default
31
+ return default unless $stdout.isatty
32
+
33
+ # clear the buffer
34
+ if ARGV&.length
35
+ ARGV.length.times do
36
+ ARGV.shift
37
+ end
38
+ end
39
+ system 'stty cbreak'
40
+
41
+ cw = white
42
+ cbw = boldwhite
43
+ cbg = boldgreen
44
+ cd = Color.default
45
+
46
+ options = unless default.nil?
47
+ "#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
48
+ else
49
+ "#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
50
+ end
51
+ $stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
52
+ res = $stdin.sysread 1
53
+ puts
54
+ system 'stty cooked'
55
+
56
+ res.chomp!
57
+ res.downcase!
58
+
59
+ return default if res.empty?
60
+
61
+ res =~ /y/i ? true : false
62
+ end
63
+ end
64
+ end
data/lib/doing/section.rb CHANGED
@@ -17,6 +17,10 @@ module Doing
17
17
  end
18
18
  end
19
19
 
20
+ def equal?(other)
21
+ @title == other.title
22
+ end
23
+
20
24
  # Outputs section title
21
25
  def to_s
22
26
  @title
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Doing
4
4
  ## Tag and search highlighting
5
- class ::String
5
+ module StringHighlight
6
6
  ## @param (see #highlight_tags)
7
7
  def highlight_tags!(color = 'yellow', last_color: nil)
8
8
  replace highlight_tags(color)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Doing
4
4
  ## Handling of search and regex strings
5
- class ::String
5
+ module StringQuery
6
6
  ##
7
7
  ## Determine whether case should be ignored for string
8
8
  ##
@@ -1,8 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'highlight'
4
+ require_relative 'query'
5
+ require_relative 'tags'
6
+ require_relative 'transform'
7
+ require_relative 'truncate'
8
+ require_relative 'url'
9
+
3
10
  class ::String
4
11
  include Doing::Color
12
+ include Doing::StringHighlight
13
+ include Doing::StringQuery
14
+ include Doing::StringTags
15
+ include Doing::StringTransform
16
+ include Doing::StringTruncate
17
+ include Doing::StringURL
5
18
 
19
+ ##
20
+ ## Force UTF-8 encoding if available
21
+ ##
22
+ ## @return [String] UTF-8 encoded string
23
+ ##
6
24
  def utf8
7
25
  if String.method_defined? :force_encoding
8
26
  dup.force_encoding('utf-8')
@@ -11,10 +29,3 @@ class ::String
11
29
  end
12
30
  end
13
31
  end
14
-
15
- require_relative 'highlight'
16
- require_relative 'query'
17
- require_relative 'tags'
18
- require_relative 'transform'
19
- require_relative 'truncate'
20
- require_relative 'url'