doing 2.1.40 → 2.1.41

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 (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'