doing 2.1.40 → 2.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +4 -4
- data/bin/commands/changes.rb +1 -1
- data/bin/commands/tag_dir.rb +49 -15
- 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 +84 -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 +2 -2
- 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 +1 -1
- 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 +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/HistoryLimitError.html +1 -1
- data/docs/doc/Doing/Errors/InvalidPlugin.html +1 -1
- data/docs/doc/Doing/Errors/MissingBackupFile.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/HTMLExport.html +256 -0
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +47 -3
- 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 +460 -11
- 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 +16 -3
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +31 -682
- 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 +565 -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 +101 -2
- data/docs/doc/Doing/TimingImport.html +285 -0
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +9 -7
- data/docs/doc/Doing/Util.html +2 -2
- data/docs/doc/Doing/Version.html +523 -0
- data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
- data/docs/doc/Doing/WWID.html +4377 -217
- 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 +324 -8
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +1 -1
- data/docs/doc/index.html +1 -1
- data/docs/doc/method_list.html +2326 -542
- data/docs/doc/top-level-namespace.html +2 -2
- data/doing.rdoc +13 -3
- data/lib/completion/_doing.zsh +1 -1
- data/lib/completion/doing.bash +2 -2
- data/lib/completion/doing.fish +3 -1
- data/lib/doing/array/array.rb +16 -12
- 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 +1 -3
- 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/completion/completion_string.rb +25 -0
- data/lib/doing/completion.rb +1 -1
- data/lib/doing/good.rb +8 -0
- data/lib/doing/item/dates.rb +1 -1
- data/lib/doing/{item.rb → item/item.rb} +10 -5
- data/lib/doing/item/query.rb +1 -1
- data/lib/doing/item/state.rb +1 -1
- data/lib/doing/item/tags.rb +1 -1
- 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/plugins/export/markdown_export.rb +4 -2
- 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 +1 -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_backup.rb +2 -2
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +357 -360
- data/lib/doing/wwid/editor.rb +173 -176
- data/lib/doing/wwid/filetools.rb +156 -159
- data/lib/doing/wwid/filter.rb +191 -183
- data/lib/doing/wwid/guess.rb +58 -60
- data/lib/doing/wwid/interactive.rb +332 -330
- data/lib/doing/wwid/modify.rb +509 -512
- data/lib/doing/wwid/tags.rb +38 -41
- data/lib/doing/wwid/timers.rb +293 -296
- data/lib/doing/{wwid.rb → wwid/wwid.rb} +32 -23
- data/lib/doing/wwid/wwidutil.rb +79 -82
- data/lib/doing.rb +5 -5
- data/lib/helpers/threaded_tests.rb +1 -0
- metadata +76 -14
- data/lib/doing/changelog.rb +0 -6
- data/lib/doing/completion/string.rb +0 -17
- data/lib/doing/items.rb +0 -221
- data/lib/doing/prompt.rb +0 -330
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
class Items < Array
|
|
5
|
+
# # Create a deep copy of Items
|
|
6
|
+
# def clone
|
|
7
|
+
# Marshal.load(Marshal.dump(self))
|
|
8
|
+
# end
|
|
9
|
+
|
|
10
|
+
def delete(item)
|
|
11
|
+
deleted = nil
|
|
12
|
+
each_with_index do |i, idx|
|
|
13
|
+
if i.equal?(item, match_section: true)
|
|
14
|
+
deleted = delete_at(idx)
|
|
15
|
+
break
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
deleted
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
## Get all tags on Items in self
|
|
23
|
+
##
|
|
24
|
+
## @return [Array] array of tags
|
|
25
|
+
##
|
|
26
|
+
def all_tags
|
|
27
|
+
each_with_object([]) do |entry, tags|
|
|
28
|
+
tags.concat(entry.tags).sort!.uniq!
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
## Return Items containing items that don't exist in
|
|
34
|
+
## receiver
|
|
35
|
+
##
|
|
36
|
+
## @param items [Items] Receiver
|
|
37
|
+
##
|
|
38
|
+
## @return [Hash] Hash of added and deleted items
|
|
39
|
+
##
|
|
40
|
+
def diff(items)
|
|
41
|
+
a = clone
|
|
42
|
+
b = items.clone
|
|
43
|
+
|
|
44
|
+
a.delete_if do |item|
|
|
45
|
+
if b.include?(item)
|
|
46
|
+
b.delete(item)
|
|
47
|
+
true
|
|
48
|
+
else
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
{ added: b, deleted: a }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
|
|
57
|
+
##
|
|
58
|
+
## @return [Items] Items array with duplicate entries removed
|
|
59
|
+
##
|
|
60
|
+
def dedup(match_section: true)
|
|
61
|
+
unique = Items.new
|
|
62
|
+
each do |item|
|
|
63
|
+
unique.push(item) unless unique.include?(item, match_section: match_section)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unique
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @see #dedup
|
|
70
|
+
def dedup!(match_section: true)
|
|
71
|
+
replace dedup(match_section: match_section)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/doing/normalize.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Doing
|
|
|
4
4
|
##
|
|
5
5
|
## String to symbol conversion
|
|
6
6
|
##
|
|
7
|
-
|
|
7
|
+
module StringNormalize
|
|
8
8
|
##
|
|
9
9
|
## Convert tag sort string to a qualified type
|
|
10
10
|
##
|
|
@@ -160,7 +160,7 @@ module Doing
|
|
|
160
160
|
##
|
|
161
161
|
## Symbol helpers
|
|
162
162
|
##
|
|
163
|
-
|
|
163
|
+
module SymbolNormalize
|
|
164
164
|
def normalize_tag_sort(default = :name)
|
|
165
165
|
to_s.normalize_tag_sort
|
|
166
166
|
end
|
|
@@ -186,3 +186,11 @@ module Doing
|
|
|
186
186
|
end
|
|
187
187
|
end
|
|
188
188
|
end
|
|
189
|
+
|
|
190
|
+
class ::String
|
|
191
|
+
include Doing::StringNormalize
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class ::Symbol
|
|
195
|
+
include Doing::SymbolNormalize
|
|
196
|
+
end
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# author: Brett Terpstra
|
|
6
6
|
# url: https://brettterpstra.com
|
|
7
7
|
module Doing
|
|
8
|
+
# @private
|
|
8
9
|
class MarkdownRenderer
|
|
9
10
|
attr_accessor :items, :page_title, :totals
|
|
10
11
|
|
|
@@ -15,12 +16,13 @@ module Doing
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def get_binding
|
|
18
|
-
binding
|
|
19
|
+
binding
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# Markdown Export Plugin
|
|
22
24
|
class MarkdownExport
|
|
23
|
-
include
|
|
25
|
+
include Util
|
|
24
26
|
|
|
25
27
|
def self.settings
|
|
26
28
|
{
|
|
@@ -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
|
+
|
|
108
|
+
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
|
|
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
|
+
raise UserCancelled
|
|
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'
|