doing 2.1.39 → 2.1.42

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