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
data/lib/doing/items.rb DELETED
@@ -1,221 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Doing
4
- # A collection of Item objects
5
- class Items < Array
6
- attr_accessor :sections
7
-
8
- def initialize
9
- super
10
- @sections = []
11
- end
12
-
13
- # List sections, title only
14
- #
15
- # @return [Array] section titles
16
- #
17
- def section_titles
18
- @sections.map(&:title)
19
- end
20
-
21
- # Test if section already exists
22
- #
23
- # @param section [String] section title
24
- #
25
- # @return [Boolean] true if section exists
26
- #
27
- def section?(section)
28
- has_section = false
29
- section = section.is_a?(Section) ? section.title.downcase : section.downcase
30
- @sections.map { |i| i.title.downcase }.include?(section)
31
- end
32
-
33
- ##
34
- ## Return the best section match for a search query
35
- ##
36
- ## @param frag The search query
37
- ## @param distance The distance apart characters can be (fuzziness)
38
- ##
39
- ## @return [Section] (first) matching section object
40
- ##
41
- def guess_section(frag, distance: 2)
42
- section = nil
43
- re = frag.to_rx(distance: distance, case_type: :ignore)
44
- @sections.each do |sect|
45
- next unless sect.title =~ /#{re}/i
46
-
47
- Doing.logger.debug('Match:', %(Assuming "#{sect.title}" from "#{frag}"))
48
- section = sect
49
- break
50
- end
51
-
52
- section
53
- end
54
-
55
- # Add a new section to the sections array. Accepts
56
- # either a Section object, or a title string that will
57
- # be converted into a Section.
58
- #
59
- # @param section [Section] The section to add. A
60
- # String value will be converted to
61
- # Section automatically.
62
- # @param log [Boolean] Add a log message
63
- # notifying the user about the
64
- # creation of the section.
65
- #
66
- # @return nothing
67
- #
68
- def add_section(section, log: false)
69
- section = section.is_a?(Section) ? section : Section.new(section.cap_first)
70
-
71
- return if section?(section)
72
-
73
- @sections.push(section)
74
- Doing.logger.info('New section:', %("#{section}" added)) if log
75
- end
76
-
77
- def delete_section(section, log: false)
78
- return unless section?(section)
79
-
80
- raise DoingRuntimeError, 'Section not empty' if in_section(section).count > 0
81
-
82
- deleted = false
83
-
84
- @sections.each do |sect|
85
- if sect.title == section && in_section(sect).count.zero?
86
- @sections.delete(sect)
87
- Doing.logger.info('Removed section:', %("#{section}" removed)) if log
88
- return
89
- end
90
- end
91
-
92
- Doing.logger.error('Not found:', %("#{section}" not found))
93
- end
94
-
95
- # Get a new Items object containing only items in a
96
- # specified section
97
- #
98
- # @param section [String] section title
99
- #
100
- # @return [Items] Array of items
101
- #
102
- def in_section(section)
103
- if section =~ /^all$/i
104
- dup
105
- else
106
- items = Items.new.concat(select { |item| !item.nil? && item.section == section })
107
- items.add_section(section, log: false)
108
- items
109
- end
110
- end
111
-
112
- ##
113
- ## Delete an item from the index
114
- ##
115
- ## @param item The item
116
- ##
117
- def delete_item(item, single: false)
118
- deleted = delete(item)
119
- Doing.logger.count(:deleted)
120
- Doing.logger.info('Entry deleted:', deleted.title) if single
121
- deleted
122
- end
123
-
124
- ##
125
- ## Update an item in the index with a modified item
126
- ##
127
- ## @param old_item The old item
128
- ## @param new_item The new item
129
- ##
130
- def update_item(old_item, new_item)
131
- s_idx = index { |item| item.equal?(old_item) }
132
-
133
- raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
134
-
135
- return if fetch(s_idx).equal?(new_item)
136
-
137
- self[s_idx] = new_item
138
- Doing.logger.count(:updated)
139
- Doing.logger.info('Entry updated:', self[s_idx].title.trunc(60))
140
- new_item
141
- end
142
-
143
- def all_tags
144
- each_with_object([]) do |entry, tags|
145
- tags.concat(entry.tags).sort!.uniq!
146
- end
147
- end
148
-
149
- ##
150
- ## Return Items containing items that don't exist in
151
- ## receiver
152
- ##
153
- ## @param items [Items] Receiver
154
- ##
155
- ## @return [Hash] Hash of added and deleted items
156
- ##
157
- def diff(items)
158
- a = clone
159
- b = items.clone
160
-
161
- a.delete_if do |item|
162
- if b.index(item)
163
- b.delete(item)
164
- true
165
- else
166
- false
167
- end
168
- end
169
- { deleted: b, added: a }
170
- end
171
-
172
- ##
173
- ## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
174
- ##
175
- ## @return [Items] Items array with duplicate entries removed
176
- ##
177
- def dedup(match_section: true)
178
- unique = Items.new
179
- each do |item|
180
- unique.push(item) unless unique.include?(item, match_section: match_section)
181
- end
182
-
183
- unique
184
- end
185
-
186
- def dedup!(match_section: true)
187
- replace dedup(match_section: match_section)
188
- end
189
-
190
- def include?(item, match_section: true)
191
- includes = false
192
- each do |other_item|
193
- if other_item.equal?(item, match_section: match_section)
194
- includes = true
195
- break
196
- end
197
- end
198
-
199
- includes
200
- end
201
-
202
- # Output sections and items in Doing file format
203
- def to_s
204
- out = []
205
- @sections.each do |section|
206
- out.push(section.original)
207
- items = in_section(section.title).sort_by { |i| [i.date, i.title] }
208
- items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
209
- items.each { |item| out.push(item.to_s) }
210
- end
211
-
212
- out.join("\n")
213
- end
214
-
215
- # @private
216
- def inspect
217
- sections = @sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')
218
- "#<Doing::Items #{count} items, #{@sections.count} sections: #{sections}>"
219
- end
220
- end
221
- end
data/lib/doing/prompt.rb DELETED
@@ -1,330 +0,0 @@
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
- ##
12
- ## Clear the terminal screen
13
- ##
14
- def clear_screen(msg = nil)
15
- puts "\e[H\e[2J" if STDOUT.tty?
16
- puts msg if msg.good?
17
- end
18
-
19
- def force_answer
20
- @force_answer ||= nil
21
- end
22
-
23
- def default_answer
24
- @default_answer ||= false
25
- end
26
-
27
- def enter_text(prompt, default_response: '')
28
- $stdin.reopen('/dev/tty')
29
- return default_response if @default_answer
30
-
31
- print "#{yellow(prompt).sub(/:?$/, ':')} #{reset}"
32
- $stdin.gets.strip
33
- end
34
-
35
- def read_line(prompt: 'Enter text', completions: [], default_response: '')
36
- $stdin.reopen('/dev/tty')
37
- return default_response if @default_answer
38
-
39
- unless completions.empty?
40
- completions.sort!
41
- comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
42
- Readline.completion_append_character = ' '
43
- Readline.completion_proc = comp
44
- end
45
-
46
- begin
47
- Readline.readline("#{yellow(prompt).sub(/:?$/, ':')} #{reset}", true).strip
48
- rescue Interrupt
49
- raise UserCancelled
50
- end
51
- end
52
-
53
- def read_lines(prompt: 'Enter text', completions: [], default_response: '')
54
- $stdin.reopen('/dev/tty')
55
- return default_response if @default_answer
56
-
57
- completions.sort!
58
- comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
59
- Readline.completion_append_character = ' '
60
- Readline.completion_proc = comp
61
- puts format(['%<promptcolor>s%<prompt>s %<textcolor>sEnter a blank line',
62
- '(%<keycolor>sreturn twice%<textcolor>s)',
63
- 'to end editing and save,',
64
- '%<keycolor>sCTRL-C%<textcolor>s to cancel%<reset>s'].join(' '),
65
- { promptcolor: boldgreen, prompt: prompt.sub(/:?$/, ':'),
66
- textcolor: yellow, keycolor: boldwhite, reset: reset })
67
-
68
- res = []
69
-
70
- begin
71
- while (line = Readline.readline('> ', true))
72
- break if line.strip.empty?
73
-
74
- res << line.chomp
75
- end
76
- rescue Interrupt
77
- raise UserCancelled
78
- end
79
-
80
- res.join("\n").strip
81
- end
82
-
83
- def request_lines(prompt: 'Enter text', default_response: '')
84
- $stdin.reopen('/dev/tty')
85
- return default_response if @default_answer
86
-
87
- ask_note = []
88
- reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
89
- puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
90
- loop do
91
- res = reader.read_line(green('> '))
92
- break if res.strip.empty?
93
-
94
- ask_note.push(res)
95
- end
96
- ask_note.join("\n").strip
97
- end
98
-
99
- ##
100
- ## Ask a yes or no question in the terminal
101
- ##
102
- ## @param question [String] The question
103
- ## to ask
104
- ## @param default_response (Bool) default
105
- ## response if no input
106
- ##
107
- ## @return (Bool) yes or no
108
- ##
109
- def yn(question, default_response: false)
110
- return @force_answer == :yes ? true : false unless @force_answer.nil?
111
-
112
- $stdin.reopen('/dev/tty')
113
-
114
- default = if default_response.is_a?(String)
115
- default_response =~ /y/i ? true : false
116
- else
117
- default_response
118
- end
119
-
120
- # if global --default is set, answer default
121
- return default if @default_answer
122
-
123
- # if this isn't an interactive shell, answer default
124
- return default unless $stdout.isatty
125
-
126
- # clear the buffer
127
- if ARGV&.length
128
- ARGV.length.times do
129
- ARGV.shift
130
- end
131
- end
132
- system 'stty cbreak'
133
-
134
- cw = white
135
- cbw = boldwhite
136
- cbg = boldgreen
137
- cd = Color.default
138
-
139
- options = unless default.nil?
140
- "#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
141
- else
142
- "#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
143
- end
144
- $stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
145
- res = $stdin.sysread 1
146
- puts
147
- system 'stty cooked'
148
-
149
- res.chomp!
150
- res.downcase!
151
-
152
- return default if res.empty?
153
-
154
- res =~ /y/i ? true : false
155
- end
156
-
157
- def fzf
158
- @fzf ||= install_fzf
159
- end
160
-
161
- def uninstall_fzf
162
- fzf_bin = File.join(File.dirname(__FILE__), '../helpers/fzf/bin/fzf')
163
- FileUtils.rm_f(fzf_bin) if File.exist?(fzf_bin)
164
- Doing.logger.warn('fzf:', "removed #{fzf_bin}")
165
- end
166
-
167
- def which_fzf
168
- fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
169
- fzf_bin = File.join(fzf_dir, 'bin/fzf')
170
- return fzf_bin if File.exist?(fzf_bin)
171
-
172
- Doing.logger.debug('fzf:', 'Using user-installed fzf')
173
- TTY::Which.which('fzf')
174
- end
175
-
176
- def silence_std(file = '/dev/null')
177
- $stdout = File.new(file, 'w')
178
- $stderr = File.new(file, 'w')
179
- end
180
-
181
- def restore_std
182
- $stdout = STDOUT
183
- $stderr = STDERR
184
- end
185
-
186
- def install_fzf(force: false)
187
- if force
188
- uninstall_fzf
189
- elsif which_fzf
190
- return which_fzf
191
- end
192
-
193
- fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
194
- FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
195
- fzf_bin = File.join(fzf_dir, 'bin/fzf')
196
- return fzf_bin if File.exist?(fzf_bin)
197
-
198
- prev_level = Doing.logger.level
199
- Doing.logger.adjust_verbosity({ log_level: :info })
200
- Doing.logger.log_now(:warn, 'fzf:', 'Compiling and installing fzf -- this will only happen once')
201
- Doing.logger.log_now(:warn, 'fzf:', 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
202
-
203
- silence_std
204
- `'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null`
205
- unless File.exist?(fzf_bin)
206
- restore_std
207
- Doing.logger.log_now(:warn, 'Error installing, trying again as root')
208
- silence_std
209
- `sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null`
210
- end
211
- restore_std
212
- unless File.exist?(fzf_bin)
213
- Doing.logger.error('fzf:', 'unable to install fzf. You can install manually and Doing will use the system version.')
214
- Doing.logger.error('fzf:', 'see https://github.com/junegunn/fzf#installation')
215
- raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues')
216
- end
217
-
218
- Doing.logger.info('fzf:', "installed to #{fzf}")
219
- Doing.logger.adjust_verbosity({ log_level: prev_level })
220
- fzf_bin
221
- end
222
-
223
- ##
224
- ## Generate a menu of options and allow user selection
225
- ##
226
- ## @return [String] The selected option
227
- ##
228
- def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
229
- return nil unless $stdout.isatty
230
-
231
- # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
232
- default_args = []
233
- default_args << %(--prompt="#{prompt}")
234
- default_args << "--height=#{options.count + 2}"
235
- default_args << '--info=inline'
236
- default_args << '--multi' if multiple
237
- header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
238
- default_args << %(--header="#{header}")
239
- default_args.concat(fzf_args)
240
- options.sort! if sorted
241
-
242
- res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{default_args.join(' ')}`
243
- return false if res.strip.size.zero?
244
-
245
- res
246
- end
247
-
248
- ##
249
- ## Create an interactive menu to select from a set of Items
250
- ##
251
- ## @param items [Array] list of items
252
- ## @param opt Additional options
253
- ##
254
- ## @option opt [Boolean] :include_section Include section name for each item in menu
255
- ## @option opt [String] :header A custom header string
256
- ## @option opt [String] :prompt A custom prompt string
257
- ## @option opt [String] :query Initial query
258
- ## @option opt [Boolean] :show_if_single Show menu even if there's only one option
259
- ## @option opt [Boolean] :menu Show menu
260
- ## @option opt [Boolean] :sort Sort options
261
- ## @option opt [Boolean] :multiple Allow multiple selections
262
- ## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
263
- ##
264
- def choose_from_items(items, **opt)
265
- return items unless $stdout.isatty
266
-
267
- return nil unless items.count.positive?
268
-
269
- case_sensitive = opt.fetch(:case, :smart).normalize_case
270
- header = opt.fetch(:header, 'Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit')
271
- prompt = opt.fetch(:prompt, 'Select entries to act on > ')
272
- query = opt.fetch(:query) { opt.fetch(:search, '') }
273
- include_section = opt.fetch(:include_section, false)
274
-
275
- pad = items.length.to_s.length
276
- options = items.map.with_index do |item, i|
277
- out = [
278
- format("%#{pad}d", i),
279
- ') ',
280
- format('%16s', item.date.strftime('%Y-%m-%d %H:%M')),
281
- ' | ',
282
- item.title
283
- ]
284
- if include_section
285
- out.concat([
286
- ' (',
287
- item.section,
288
- ') '
289
- ])
290
- end
291
- out.join('')
292
- end
293
-
294
- fzf_args = [
295
- %(--header="#{header}"),
296
- %(--prompt="#{prompt.sub(/ *$/, ' ')}"),
297
- opt.fetch(:multiple) ? '--multi' : '--no-multi',
298
- '-0',
299
- '--bind ctrl-a:select-all',
300
- %(-q "#{query}"),
301
- '--info=inline'
302
- ]
303
- fzf_args.push('-1') unless opt.fetch(:show_if_single, false)
304
- fzf_args << case case_sensitive
305
- when :sensitive
306
- '+i'
307
- when :ignore
308
- '-i'
309
- end
310
- fzf_args << '-e' if opt.fetch(:exact, false)
311
-
312
-
313
- unless opt.fetch(:menu)
314
- raise InvalidArgument, "Can't skip menu when no query is provided" unless query && !query.empty?
315
-
316
- fzf_args.concat([%(--filter="#{query}"), opt.fetch(:sort) ? '' : '--no-sort'])
317
- end
318
-
319
- res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
320
- selected = []
321
- res.split(/\n/).each do |item|
322
- idx = item.match(/^ *(\d+)\)/)[1].to_i
323
- selected.push(items[idx])
324
- end
325
-
326
- opt.fetch(:multiple) ? selected : selected[0]
327
- end
328
- end
329
- end
330
- end