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