doing 2.1.39 → 2.1.42

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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +67 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +1 -1
  6. data/Rakefile +4 -4
  7. data/bin/commands/again.rb +1 -3
  8. data/bin/commands/changes.rb +50 -34
  9. data/bin/commands/commands.rb +77 -52
  10. data/bin/commands/commands_accepting.rb +57 -53
  11. data/bin/commands/config.rb +45 -36
  12. data/bin/commands/done.rb +1 -18
  13. data/bin/commands/finish.rb +90 -59
  14. data/bin/commands/flag.rb +5 -1
  15. data/bin/commands/grep.rb +3 -14
  16. data/bin/commands/last.rb +2 -8
  17. data/bin/commands/meanwhile.rb +13 -6
  18. data/bin/commands/now.rb +151 -107
  19. data/bin/commands/on.rb +8 -18
  20. data/bin/commands/recent.rb +2 -8
  21. data/bin/commands/reset.rb +24 -1
  22. data/bin/commands/select.rb +1 -1
  23. data/bin/commands/show.rb +6 -17
  24. data/bin/commands/since.rb +1 -12
  25. data/bin/commands/tag_dir.rb +49 -15
  26. data/bin/commands/today.rb +2 -13
  27. data/bin/commands/undo.rb +4 -6
  28. data/bin/commands/view.rb +1 -1
  29. data/bin/commands/yesterday.rb +2 -13
  30. data/bin/doing +15 -8
  31. data/{Dockerfile → docker/Dockerfile} +3 -1
  32. data/{Dockerfile-2.6 → docker/Dockerfile-2.6} +2 -2
  33. data/{Dockerfile-2.7 → docker/Dockerfile-2.7} +2 -2
  34. data/{Dockerfile-3.0 → docker/Dockerfile-3.0} +2 -2
  35. data/{bash_profile → docker/bash_profile} +0 -0
  36. data/{inputrc → docker/inputrc} +0 -0
  37. data/docs/doc/Array.html +85 -2
  38. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  39. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  40. data/docs/doc/BooleanTermParser/Query.html +1 -1
  41. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  42. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  43. data/docs/doc/BooleanTermParser.html +1 -1
  44. data/docs/doc/Doing/ArrayNestedHash.html +198 -0
  45. data/docs/doc/Doing/ArrayTags.html +424 -0
  46. data/docs/doc/Doing/CSVExport.html +266 -0
  47. data/docs/doc/Doing/CalendarImport.html +232 -0
  48. data/docs/doc/Doing/Change.html +617 -0
  49. data/docs/doc/Doing/Changes.html +468 -0
  50. data/docs/doc/Doing/ChronifyArray.html +347 -0
  51. data/docs/doc/Doing/ChronifyNumeric.html +271 -0
  52. data/docs/doc/Doing/ChronifyString.html +682 -0
  53. data/docs/doc/Doing/Color.html +167 -21
  54. data/docs/doc/Doing/Completion/BashCompletions.html +445 -0
  55. data/docs/doc/Doing/Completion/FishCompletions.html +445 -0
  56. data/docs/doc/Doing/Completion/StringUtils.html +229 -0
  57. data/docs/doc/Doing/Completion/ZshCompletions.html +445 -0
  58. data/docs/doc/Doing/Completion.html +17 -3
  59. data/docs/doc/Doing/Configuration.html +3 -2
  60. data/docs/doc/Doing/DayOneRenderer.html +383 -0
  61. data/docs/doc/Doing/DayoneExport.html +290 -0
  62. data/docs/doc/Doing/DoingImport.html +391 -0
  63. data/docs/doc/Doing/Entry.html +381 -0
  64. data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
  65. data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
  66. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  67. data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
  68. data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
  69. data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
  70. data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
  71. data/docs/doc/Doing/Errors/NoResults.html +10 -2
  72. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  73. data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
  74. data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
  75. data/docs/doc/Doing/Errors.html +9 -9
  76. data/docs/doc/Doing/HTMLExport.html +256 -0
  77. data/docs/doc/Doing/Hooks.html +1 -1
  78. data/docs/doc/Doing/Item.html +179 -1660
  79. data/docs/doc/Doing/ItemDates.html +564 -0
  80. data/docs/doc/Doing/ItemQuery.html +614 -0
  81. data/docs/doc/Doing/ItemState.html +387 -0
  82. data/docs/doc/Doing/ItemTags.html +498 -0
  83. data/docs/doc/Doing/Items.html +581 -15
  84. data/docs/doc/Doing/JSONExport.html +222 -0
  85. data/docs/doc/Doing/Logger.html +1 -1
  86. data/docs/doc/Doing/MarkdownExport.html +266 -0
  87. data/docs/doc/Doing/MarkdownRenderer.html +383 -0
  88. data/docs/doc/Doing/Note.html +18 -4
  89. data/docs/doc/Doing/Pager.html +1 -1
  90. data/docs/doc/Doing/Plugins.html +181 -76
  91. data/docs/doc/Doing/Prompt.html +32 -683
  92. data/docs/doc/Doing/PromptChoose.html +484 -0
  93. data/docs/doc/Doing/PromptFZF.html +391 -0
  94. data/docs/doc/Doing/PromptInput.html +572 -0
  95. data/docs/doc/Doing/PromptSTD.html +293 -0
  96. data/docs/doc/Doing/PromptYN.html +237 -0
  97. data/docs/doc/Doing/Section.html +58 -2
  98. data/docs/doc/Doing/StringHighlight.html +533 -0
  99. data/docs/doc/Doing/StringNormalize.html +929 -0
  100. data/docs/doc/Doing/StringQuery.html +725 -0
  101. data/docs/doc/Doing/StringTags.html +884 -0
  102. data/docs/doc/Doing/StringTransform.html +599 -0
  103. data/docs/doc/Doing/StringTruncate.html +448 -0
  104. data/docs/doc/Doing/StringURL.html +409 -0
  105. data/docs/doc/Doing/SymbolNormalize.html +341 -0
  106. data/docs/doc/Doing/TaskPaperExport.html +222 -0
  107. data/docs/doc/Doing/TemplateExport.html +249 -0
  108. data/docs/doc/Doing/TemplateString.html +102 -3
  109. data/docs/doc/Doing/TimingImport.html +285 -0
  110. data/docs/doc/Doing/Types.html +1 -1
  111. data/docs/doc/Doing/Util/Backup.html +11 -163
  112. data/docs/doc/Doing/Util.html +67 -10
  113. data/docs/doc/Doing/Version.html +523 -0
  114. data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
  115. data/docs/doc/Doing/WWID.html +476 -139
  116. data/docs/doc/Doing/WWIDDisplay.html +865 -0
  117. data/docs/doc/Doing/WWIDEditor.html +466 -0
  118. data/docs/doc/Doing/WWIDFileTools.html +359 -0
  119. data/docs/doc/Doing/WWIDFilter.html +466 -0
  120. data/docs/doc/Doing/WWIDGuess.html +299 -0
  121. data/docs/doc/Doing/WWIDInteractive.html +752 -0
  122. data/docs/doc/Doing/WWIDModify.html +1078 -0
  123. data/docs/doc/Doing/WWIDTags.html +302 -0
  124. data/docs/doc/Doing/WWIDTimers.html +359 -0
  125. data/docs/doc/Doing/WWIDUtil.html +510 -0
  126. data/docs/doc/Doing.html +9 -6
  127. data/docs/doc/FalseClass.html +1 -1
  128. data/docs/doc/GLI/Commands/Help.html +1 -1
  129. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  130. data/docs/doc/GLI/Commands.html +1 -1
  131. data/docs/doc/GLI.html +1 -1
  132. data/docs/doc/Hash.html +1 -1
  133. data/docs/doc/Numeric.html +23 -78
  134. data/docs/doc/Object.html +1 -1
  135. data/docs/doc/PhraseParser/Operator.html +1 -1
  136. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  137. data/docs/doc/PhraseParser/Query.html +1 -1
  138. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  139. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  140. data/docs/doc/PhraseParser/TermClause.html +1 -1
  141. data/docs/doc/PhraseParser.html +1 -1
  142. data/docs/doc/Status.html +1 -1
  143. data/docs/doc/String.html +58 -633
  144. data/docs/doc/Symbol.html +9 -224
  145. data/docs/doc/Time.html +119 -13
  146. data/docs/doc/TrueClass.html +1 -1
  147. data/docs/doc/_index.html +348 -4
  148. data/docs/doc/class_list.html +1 -1
  149. data/docs/doc/file.README.html +2 -2
  150. data/docs/doc/index.html +2 -2
  151. data/docs/doc/method_list.html +1904 -592
  152. data/docs/doc/top-level-namespace.html +12 -4
  153. data/docs/index.md +1 -1
  154. data/doing.rdoc +67 -15
  155. data/lib/completion/_doing.zsh +6 -6
  156. data/lib/completion/doing.bash +10 -10
  157. data/lib/completion/doing.fish +10 -3
  158. data/lib/doing/add_options.rb +39 -1
  159. data/lib/doing/array/array.rb +18 -12
  160. data/lib/doing/array/cleanup.rb +31 -0
  161. data/lib/doing/array/nested_hash.rb +1 -1
  162. data/lib/doing/array/tags.rb +6 -5
  163. data/lib/doing/changelog/changelog.rb +6 -0
  164. data/lib/doing/chronify/array.rb +65 -25
  165. data/lib/doing/chronify/chronify.rb +12 -0
  166. data/lib/doing/chronify/numeric.rb +3 -2
  167. data/lib/doing/chronify/string.rb +1 -1
  168. data/lib/doing/colors.rb +77 -30
  169. data/lib/doing/completion/completion_string.rb +25 -0
  170. data/lib/doing/completion.rb +4 -5
  171. data/lib/doing/configuration.rb +7 -3
  172. data/lib/doing/errors.rb +51 -35
  173. data/lib/doing/good.rb +8 -0
  174. data/lib/doing/hooks.rb +3 -3
  175. data/lib/doing/item/dates.rb +112 -0
  176. data/lib/doing/item/item.rb +128 -0
  177. data/lib/doing/{item.rb → item/query.rb} +2 -353
  178. data/lib/doing/item/state.rb +59 -0
  179. data/lib/doing/item/tags.rb +87 -0
  180. data/lib/doing/items/filter.rb +67 -0
  181. data/lib/doing/items/items.rb +57 -0
  182. data/lib/doing/items/modify.rb +36 -0
  183. data/lib/doing/items/sections.rb +83 -0
  184. data/lib/doing/items/util.rb +74 -0
  185. data/lib/doing/normalize.rb +10 -2
  186. data/lib/doing/note.rb +1 -1
  187. data/lib/doing/pager.rb +9 -3
  188. data/lib/doing/plugin_manager.rb +33 -8
  189. data/lib/doing/plugins/export/markdown_export.rb +4 -2
  190. data/lib/doing/plugins/export/template_export.rb +4 -4
  191. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  192. data/lib/doing/plugins/import/doing_import.rb +1 -1
  193. data/lib/doing/prompt/choose.rb +118 -0
  194. data/lib/doing/prompt/fzf.rb +84 -0
  195. data/lib/doing/prompt/input.rb +129 -0
  196. data/lib/doing/prompt/prompt.rb +41 -0
  197. data/lib/doing/prompt/std.rb +32 -0
  198. data/lib/doing/prompt/yn.rb +64 -0
  199. data/lib/doing/section.rb +4 -0
  200. data/lib/doing/string/highlight.rb +1 -1
  201. data/lib/doing/string/query.rb +1 -1
  202. data/lib/doing/string/string.rb +18 -7
  203. data/lib/doing/string/tags.rb +14 -3
  204. data/lib/doing/string/transform.rb +7 -1
  205. data/lib/doing/string/truncate.rb +1 -1
  206. data/lib/doing/string/url.rb +1 -1
  207. data/lib/doing/time.rb +19 -1
  208. data/lib/doing/util.rb +12 -6
  209. data/lib/doing/util_backup.rb +62 -57
  210. data/lib/doing/version.rb +1 -1
  211. data/lib/doing/wwid/display.rb +396 -0
  212. data/lib/doing/wwid/editor.rb +214 -0
  213. data/lib/doing/wwid/filetools.rb +183 -0
  214. data/lib/doing/wwid/filter.rb +226 -0
  215. data/lib/doing/wwid/guess.rb +85 -0
  216. data/lib/doing/wwid/interactive.rb +377 -0
  217. data/lib/doing/wwid/modify.rb +617 -0
  218. data/lib/doing/wwid/tags.rb +51 -0
  219. data/lib/doing/wwid/timers.rb +342 -0
  220. data/lib/doing/wwid/wwid.rb +121 -0
  221. data/lib/doing/wwid/wwidutil.rb +101 -0
  222. data/lib/doing.rb +7 -7
  223. data/lib/helpers/threaded_tests.rb +1 -0
  224. metadata +94 -14
  225. data/lib/doing/changelog.rb +0 -6
  226. data/lib/doing/completion/string.rb +0 -17
  227. data/lib/doing/items.rb +0 -196
  228. data/lib/doing/prompt.rb +0 -330
  229. data/lib/doing/wwid.rb +0 -2398
@@ -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
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
108
+
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
+ return nil
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'
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Doing
4
4
  # Handling of @tags in strings
5
- class ::String
5
+ module StringTags
6
6
  ##
7
7
  ## Add @ prefix to string if needed, maintains +/- prefix
8
8
  ##
@@ -21,6 +21,17 @@ module Doing
21
21
  strip.sub(/^([+-]*)@?/, '\1')
22
22
  end
23
23
 
24
+ ##
25
+ ## Split a string of tags, remove @ symbols, with or
26
+ ## without @ symbols, with or without parenthetical
27
+ ## values
28
+ ##
29
+ ## @return [Array] array of tags without @ symbols
30
+ ##
31
+ def split_tags
32
+ gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).map(&:remove_at).sort.uniq
33
+ end
34
+
24
35
  ##
25
36
  ## Convert a list of tags to an array. Tags can be with
26
37
  ## or without @ symbols, separated by any character, and
@@ -29,7 +40,7 @@ module Doing
29
40
  ## @return [Array] array of tags including @ symbols
30
41
  ##
31
42
  def to_tags
32
- arr = gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
43
+ arr = split_tags.map(&:add_at)
33
44
  if block_given?
34
45
  yield arr
35
46
  else
@@ -38,7 +49,7 @@ module Doing
38
49
  end
39
50
 
40
51
  ##
41
- ## @brief Adds tags to a string
52
+ ## Adds tags to a string
42
53
  ##
43
54
  ## @param tags [String or Array] List of tags to add. @ symbol optional
44
55
  ## @param remove [Boolean] remove tags instead of adding
@@ -4,7 +4,7 @@ module Doing
4
4
  ##
5
5
  ## String helpers
6
6
  ##
7
- class ::String
7
+ module StringTransform
8
8
  # Compress multiple spaces to single space
9
9
  def compress
10
10
  gsub(/ +/, ' ').strip
@@ -164,5 +164,11 @@ module Doing
164
164
  end
165
165
  end
166
166
  end
167
+
168
+ def titlecase
169
+ tr('_', ' ').
170
+ gsub(/\s+/, ' ').
171
+ gsub(/\b\w/){ $`[-1,1] == "'" ? $& : $&.upcase }
172
+ end
167
173
  end
168
174
  end
@@ -4,7 +4,7 @@ module Doing
4
4
  ##
5
5
  ## String truncation
6
6
  ##
7
- class ::String
7
+ module StringTruncate
8
8
  ##
9
9
  ## Truncate to nearest word
10
10
  ##
@@ -4,7 +4,7 @@ module Doing
4
4
  ##
5
5
  ## URL linking and formatting
6
6
  ##
7
- class ::String
7
+ module StringURL
8
8
  ##
9
9
  ## Turn raw urls into HTML links
10
10
  ##
data/lib/doing/time.rb CHANGED
@@ -3,18 +3,31 @@ module Doing
3
3
  ## Date helpers
4
4
  ##
5
5
  class ::Time
6
+ # Format time as a relative date. Dates from today get
7
+ # just a time, from the last week get a time and day,
8
+ # from the last year get a month/day/time, and older
9
+ # entries get month/day/year/time
10
+ #
11
+ # @return [String] formatted date
12
+ #
6
13
  def relative_date
7
14
  if self > Date.today.to_time
8
15
  strftime('%_I:%M%P')
9
16
  elsif self > (Date.today - 6).to_time
10
17
  strftime('%a %_I:%M%P')
11
- elsif self.year == Date.today.year || (self.year + 1 == Date.today.year && self.month > Date.today.month)
18
+ elsif year == Date.today.year || (year + 1 == Date.today.year && month > Date.today.month)
12
19
  strftime('%m/%d %_I:%M%P')
13
20
  else
14
21
  strftime('%m/%d/%y %_I:%M%P')
15
22
  end
16
23
  end
17
24
 
25
+ ##
26
+ ## Format seconds as a natural language string
27
+ ##
28
+ ## @param seconds [Integer] number of seconds
29
+ ##
30
+ ## @return [String] Date formatted as "X days, X hours, X minutes, X seconds"
18
31
  def humanize(seconds)
19
32
  s = seconds
20
33
  m = (s / 60).floor
@@ -32,6 +45,11 @@ module Doing
32
45
  output.join(', ')
33
46
  end
34
47
 
48
+ ##
49
+ ## Format date as "X hours ago"
50
+ ##
51
+ ## @return [String] Formatted date
52
+ ##
35
53
  def time_ago
36
54
  if self > Date.today.to_time
37
55
  output = humanize(Time.now - self)