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.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +4 -4
- data/bin/commands/again.rb +1 -3
- data/bin/commands/changes.rb +50 -34
- data/bin/commands/commands.rb +77 -52
- data/bin/commands/commands_accepting.rb +57 -53
- data/bin/commands/config.rb +45 -36
- data/bin/commands/done.rb +1 -18
- data/bin/commands/finish.rb +90 -59
- data/bin/commands/flag.rb +5 -1
- data/bin/commands/grep.rb +3 -14
- data/bin/commands/last.rb +2 -8
- data/bin/commands/meanwhile.rb +13 -6
- data/bin/commands/now.rb +151 -107
- data/bin/commands/on.rb +8 -18
- data/bin/commands/recent.rb +2 -8
- data/bin/commands/reset.rb +24 -1
- data/bin/commands/select.rb +1 -1
- data/bin/commands/show.rb +6 -17
- data/bin/commands/since.rb +1 -12
- data/bin/commands/tag_dir.rb +49 -15
- data/bin/commands/today.rb +2 -13
- data/bin/commands/undo.rb +4 -6
- data/bin/commands/view.rb +1 -1
- data/bin/commands/yesterday.rb +2 -13
- data/bin/doing +15 -8
- data/{Dockerfile → docker/Dockerfile} +3 -1
- data/{Dockerfile-2.6 → docker/Dockerfile-2.6} +2 -2
- data/{Dockerfile-2.7 → docker/Dockerfile-2.7} +2 -2
- data/{Dockerfile-3.0 → docker/Dockerfile-3.0} +2 -2
- data/{bash_profile → docker/bash_profile} +0 -0
- data/{inputrc → docker/inputrc} +0 -0
- data/docs/doc/Array.html +85 -2
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/ArrayNestedHash.html +198 -0
- data/docs/doc/Doing/ArrayTags.html +424 -0
- data/docs/doc/Doing/CSVExport.html +266 -0
- data/docs/doc/Doing/CalendarImport.html +232 -0
- data/docs/doc/Doing/Change.html +617 -0
- data/docs/doc/Doing/Changes.html +468 -0
- data/docs/doc/Doing/ChronifyArray.html +347 -0
- data/docs/doc/Doing/ChronifyNumeric.html +271 -0
- data/docs/doc/Doing/ChronifyString.html +682 -0
- data/docs/doc/Doing/Color.html +167 -21
- data/docs/doc/Doing/Completion/BashCompletions.html +445 -0
- data/docs/doc/Doing/Completion/FishCompletions.html +445 -0
- data/docs/doc/Doing/Completion/StringUtils.html +229 -0
- data/docs/doc/Doing/Completion/ZshCompletions.html +445 -0
- data/docs/doc/Doing/Completion.html +17 -3
- data/docs/doc/Doing/Configuration.html +3 -2
- data/docs/doc/Doing/DayOneRenderer.html +383 -0
- data/docs/doc/Doing/DayoneExport.html +290 -0
- data/docs/doc/Doing/DoingImport.html +391 -0
- data/docs/doc/Doing/Entry.html +381 -0
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
- data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
- data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
- data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
- data/docs/doc/Doing/Errors/NoResults.html +10 -2
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
- data/docs/doc/Doing/Errors.html +9 -9
- data/docs/doc/Doing/HTMLExport.html +256 -0
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +179 -1660
- data/docs/doc/Doing/ItemDates.html +564 -0
- data/docs/doc/Doing/ItemQuery.html +614 -0
- data/docs/doc/Doing/ItemState.html +387 -0
- data/docs/doc/Doing/ItemTags.html +498 -0
- data/docs/doc/Doing/Items.html +581 -15
- data/docs/doc/Doing/JSONExport.html +222 -0
- data/docs/doc/Doing/Logger.html +1 -1
- data/docs/doc/Doing/MarkdownExport.html +266 -0
- data/docs/doc/Doing/MarkdownRenderer.html +383 -0
- data/docs/doc/Doing/Note.html +18 -4
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +181 -76
- data/docs/doc/Doing/Prompt.html +32 -683
- data/docs/doc/Doing/PromptChoose.html +484 -0
- data/docs/doc/Doing/PromptFZF.html +391 -0
- data/docs/doc/Doing/PromptInput.html +572 -0
- data/docs/doc/Doing/PromptSTD.html +293 -0
- data/docs/doc/Doing/PromptYN.html +237 -0
- data/docs/doc/Doing/Section.html +58 -2
- data/docs/doc/Doing/StringHighlight.html +533 -0
- data/docs/doc/Doing/StringNormalize.html +929 -0
- data/docs/doc/Doing/StringQuery.html +725 -0
- data/docs/doc/Doing/StringTags.html +884 -0
- data/docs/doc/Doing/StringTransform.html +599 -0
- data/docs/doc/Doing/StringTruncate.html +448 -0
- data/docs/doc/Doing/StringURL.html +409 -0
- data/docs/doc/Doing/SymbolNormalize.html +341 -0
- data/docs/doc/Doing/TaskPaperExport.html +222 -0
- data/docs/doc/Doing/TemplateExport.html +249 -0
- data/docs/doc/Doing/TemplateString.html +102 -3
- data/docs/doc/Doing/TimingImport.html +285 -0
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +11 -163
- data/docs/doc/Doing/Util.html +67 -10
- data/docs/doc/Doing/Version.html +523 -0
- data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
- data/docs/doc/Doing/WWID.html +476 -139
- data/docs/doc/Doing/WWIDDisplay.html +865 -0
- data/docs/doc/Doing/WWIDEditor.html +466 -0
- data/docs/doc/Doing/WWIDFileTools.html +359 -0
- data/docs/doc/Doing/WWIDFilter.html +466 -0
- data/docs/doc/Doing/WWIDGuess.html +299 -0
- data/docs/doc/Doing/WWIDInteractive.html +752 -0
- data/docs/doc/Doing/WWIDModify.html +1078 -0
- data/docs/doc/Doing/WWIDTags.html +302 -0
- data/docs/doc/Doing/WWIDTimers.html +359 -0
- data/docs/doc/Doing/WWIDUtil.html +510 -0
- data/docs/doc/Doing.html +9 -6
- data/docs/doc/FalseClass.html +1 -1
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Numeric.html +23 -78
- data/docs/doc/Object.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +58 -633
- data/docs/doc/Symbol.html +9 -224
- data/docs/doc/Time.html +119 -13
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +348 -4
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +1904 -592
- data/docs/doc/top-level-namespace.html +12 -4
- data/docs/index.md +1 -1
- data/doing.rdoc +67 -15
- data/lib/completion/_doing.zsh +6 -6
- data/lib/completion/doing.bash +10 -10
- data/lib/completion/doing.fish +10 -3
- data/lib/doing/add_options.rb +39 -1
- data/lib/doing/array/array.rb +18 -12
- data/lib/doing/array/cleanup.rb +31 -0
- data/lib/doing/array/nested_hash.rb +1 -1
- data/lib/doing/array/tags.rb +6 -5
- data/lib/doing/changelog/changelog.rb +6 -0
- data/lib/doing/chronify/array.rb +65 -25
- data/lib/doing/chronify/chronify.rb +12 -0
- data/lib/doing/chronify/numeric.rb +3 -2
- data/lib/doing/chronify/string.rb +1 -1
- data/lib/doing/colors.rb +77 -30
- data/lib/doing/completion/completion_string.rb +25 -0
- data/lib/doing/completion.rb +4 -5
- data/lib/doing/configuration.rb +7 -3
- data/lib/doing/errors.rb +51 -35
- data/lib/doing/good.rb +8 -0
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item/dates.rb +112 -0
- data/lib/doing/item/item.rb +128 -0
- data/lib/doing/{item.rb → item/query.rb} +2 -353
- data/lib/doing/item/state.rb +59 -0
- data/lib/doing/item/tags.rb +87 -0
- data/lib/doing/items/filter.rb +67 -0
- data/lib/doing/items/items.rb +57 -0
- data/lib/doing/items/modify.rb +36 -0
- data/lib/doing/items/sections.rb +83 -0
- data/lib/doing/items/util.rb +74 -0
- data/lib/doing/normalize.rb +10 -2
- data/lib/doing/note.rb +1 -1
- data/lib/doing/pager.rb +9 -3
- data/lib/doing/plugin_manager.rb +33 -8
- data/lib/doing/plugins/export/markdown_export.rb +4 -2
- data/lib/doing/plugins/export/template_export.rb +4 -4
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/plugins/import/doing_import.rb +1 -1
- data/lib/doing/prompt/choose.rb +118 -0
- data/lib/doing/prompt/fzf.rb +84 -0
- data/lib/doing/prompt/input.rb +129 -0
- data/lib/doing/prompt/prompt.rb +41 -0
- data/lib/doing/prompt/std.rb +32 -0
- data/lib/doing/prompt/yn.rb +64 -0
- data/lib/doing/section.rb +4 -0
- data/lib/doing/string/highlight.rb +1 -1
- data/lib/doing/string/query.rb +1 -1
- data/lib/doing/string/string.rb +18 -7
- data/lib/doing/string/tags.rb +14 -3
- data/lib/doing/string/transform.rb +7 -1
- data/lib/doing/string/truncate.rb +1 -1
- data/lib/doing/string/url.rb +1 -1
- data/lib/doing/time.rb +19 -1
- data/lib/doing/util.rb +12 -6
- data/lib/doing/util_backup.rb +62 -57
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +396 -0
- data/lib/doing/wwid/editor.rb +214 -0
- data/lib/doing/wwid/filetools.rb +183 -0
- data/lib/doing/wwid/filter.rb +226 -0
- data/lib/doing/wwid/guess.rb +85 -0
- data/lib/doing/wwid/interactive.rb +377 -0
- data/lib/doing/wwid/modify.rb +617 -0
- data/lib/doing/wwid/tags.rb +51 -0
- data/lib/doing/wwid/timers.rb +342 -0
- data/lib/doing/wwid/wwid.rb +121 -0
- data/lib/doing/wwid/wwidutil.rb +101 -0
- data/lib/doing.rb +7 -7
- data/lib/helpers/threaded_tests.rb +1 -0
- metadata +94 -14
- data/lib/doing/changelog.rb +0 -6
- data/lib/doing/completion/string.rb +0 -17
- data/lib/doing/items.rb +0 -196
- data/lib/doing/prompt.rb +0 -330
- 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
data/lib/doing/string/query.rb
CHANGED
data/lib/doing/string/string.rb
CHANGED
|
@@ -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'
|
data/lib/doing/string/tags.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Doing
|
|
4
4
|
# Handling of @tags in strings
|
|
5
|
-
|
|
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 =
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
data/lib/doing/string/url.rb
CHANGED
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
|
|
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)
|