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
data/lib/doing/items.rb
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Doing
|
|
4
|
-
# A collection of Item objects
|
|
5
|
-
class Items < Array
|
|
6
|
-
attr_accessor :sections
|
|
7
|
-
|
|
8
|
-
def initialize
|
|
9
|
-
super
|
|
10
|
-
@sections = []
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# List sections, title only
|
|
14
|
-
#
|
|
15
|
-
# @return [Array] section titles
|
|
16
|
-
#
|
|
17
|
-
def section_titles
|
|
18
|
-
@sections.map(&:title)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Test if section already exists
|
|
22
|
-
#
|
|
23
|
-
# @param section [String] section title
|
|
24
|
-
#
|
|
25
|
-
# @return [Boolean] true if section exists
|
|
26
|
-
#
|
|
27
|
-
def section?(section)
|
|
28
|
-
has_section = false
|
|
29
|
-
section = section.is_a?(Section) ? section.title.downcase : section.downcase
|
|
30
|
-
@sections.map { |i| i.title.downcase }.include?(section)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
##
|
|
34
|
-
## Return the best section match for a search query
|
|
35
|
-
##
|
|
36
|
-
## @param frag The search query
|
|
37
|
-
## @param distance The distance apart characters can be (fuzziness)
|
|
38
|
-
##
|
|
39
|
-
## @return [Section] (first) matching section object
|
|
40
|
-
##
|
|
41
|
-
def guess_section(frag, distance: 2)
|
|
42
|
-
section = nil
|
|
43
|
-
re = frag.to_rx(distance: distance, case_type: :ignore)
|
|
44
|
-
@sections.each do |sect|
|
|
45
|
-
next unless sect.title =~ /#{re}/i
|
|
46
|
-
|
|
47
|
-
Doing.logger.debug('Match:', %(Assuming "#{sect.title}" from "#{frag}"))
|
|
48
|
-
section = sect
|
|
49
|
-
break
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
section
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Add a new section to the sections array. Accepts
|
|
56
|
-
# either a Section object, or a title string that will
|
|
57
|
-
# be converted into a Section.
|
|
58
|
-
#
|
|
59
|
-
# @param section [Section] The section to add. A
|
|
60
|
-
# String value will be converted to
|
|
61
|
-
# Section automatically.
|
|
62
|
-
# @param log [Boolean] Add a log message
|
|
63
|
-
# notifying the user about the
|
|
64
|
-
# creation of the section.
|
|
65
|
-
#
|
|
66
|
-
# @return nothing
|
|
67
|
-
#
|
|
68
|
-
def add_section(section, log: false)
|
|
69
|
-
section = section.is_a?(Section) ? section : Section.new(section.cap_first)
|
|
70
|
-
|
|
71
|
-
return if section?(section)
|
|
72
|
-
|
|
73
|
-
@sections.push(section)
|
|
74
|
-
Doing.logger.info('New section:', %("#{section}" added)) if log
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def delete_section(section, log: false)
|
|
78
|
-
return unless section?(section)
|
|
79
|
-
|
|
80
|
-
raise DoingRuntimeError, 'Section not empty' if in_section(section).count > 0
|
|
81
|
-
|
|
82
|
-
deleted = false
|
|
83
|
-
|
|
84
|
-
@sections.each do |sect|
|
|
85
|
-
if sect.title == section && in_section(sect).count.zero?
|
|
86
|
-
@sections.delete(sect)
|
|
87
|
-
Doing.logger.info('Removed section:', %("#{section}" removed)) if log
|
|
88
|
-
return
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
Doing.logger.error('Not found:', %("#{section}" not found))
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Get a new Items object containing only items in a
|
|
96
|
-
# specified section
|
|
97
|
-
#
|
|
98
|
-
# @param section [String] section title
|
|
99
|
-
#
|
|
100
|
-
# @return [Items] Array of items
|
|
101
|
-
#
|
|
102
|
-
def in_section(section)
|
|
103
|
-
if section =~ /^all$/i
|
|
104
|
-
dup
|
|
105
|
-
else
|
|
106
|
-
items = Items.new.concat(select { |item| !item.nil? && item.section == section })
|
|
107
|
-
items.add_section(section, log: false)
|
|
108
|
-
items
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
##
|
|
113
|
-
## Delete an item from the index
|
|
114
|
-
##
|
|
115
|
-
## @param item The item
|
|
116
|
-
##
|
|
117
|
-
def delete_item(item, single: false)
|
|
118
|
-
deleted = delete(item)
|
|
119
|
-
Doing.logger.count(:deleted)
|
|
120
|
-
Doing.logger.info('Entry deleted:', deleted.title) if single
|
|
121
|
-
deleted
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
##
|
|
125
|
-
## Update an item in the index with a modified item
|
|
126
|
-
##
|
|
127
|
-
## @param old_item The old item
|
|
128
|
-
## @param new_item The new item
|
|
129
|
-
##
|
|
130
|
-
def update_item(old_item, new_item)
|
|
131
|
-
s_idx = index { |item| item.equal?(old_item) }
|
|
132
|
-
|
|
133
|
-
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
|
134
|
-
|
|
135
|
-
return if fetch(s_idx).equal?(new_item)
|
|
136
|
-
|
|
137
|
-
self[s_idx] = new_item
|
|
138
|
-
Doing.logger.count(:updated)
|
|
139
|
-
Doing.logger.info('Entry updated:', self[s_idx].title.trunc(60))
|
|
140
|
-
new_item
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def all_tags
|
|
144
|
-
each_with_object([]) do |entry, tags|
|
|
145
|
-
tags.concat(entry.tags).sort!.uniq!
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
##
|
|
150
|
-
## Return Items containing items that don't exist in
|
|
151
|
-
## receiver
|
|
152
|
-
##
|
|
153
|
-
## @param items [Items] Receiver
|
|
154
|
-
##
|
|
155
|
-
## @return [Hash] Hash of added and deleted items
|
|
156
|
-
##
|
|
157
|
-
def diff(items)
|
|
158
|
-
a = clone
|
|
159
|
-
b = items.clone
|
|
160
|
-
|
|
161
|
-
a.delete_if do |item|
|
|
162
|
-
if b.index(item)
|
|
163
|
-
b.delete(item)
|
|
164
|
-
true
|
|
165
|
-
else
|
|
166
|
-
false
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
{ deleted: b, added: a }
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
##
|
|
173
|
-
## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
|
|
174
|
-
##
|
|
175
|
-
## @return [Items] Items array with duplicate entries removed
|
|
176
|
-
##
|
|
177
|
-
def dedup(match_section: true)
|
|
178
|
-
unique = Items.new
|
|
179
|
-
each do |item|
|
|
180
|
-
unique.push(item) unless unique.include?(item, match_section: match_section)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
unique
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def dedup!(match_section: true)
|
|
187
|
-
replace dedup(match_section: match_section)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def include?(item, match_section: true)
|
|
191
|
-
includes = false
|
|
192
|
-
each do |other_item|
|
|
193
|
-
if other_item.equal?(item, match_section: match_section)
|
|
194
|
-
includes = true
|
|
195
|
-
break
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
includes
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# Output sections and items in Doing file format
|
|
203
|
-
def to_s
|
|
204
|
-
out = []
|
|
205
|
-
@sections.each do |section|
|
|
206
|
-
out.push(section.original)
|
|
207
|
-
items = in_section(section.title).sort_by { |i| [i.date, i.title] }
|
|
208
|
-
items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
|
|
209
|
-
items.each { |item| out.push(item.to_s) }
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
out.join("\n")
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# @private
|
|
216
|
-
def inspect
|
|
217
|
-
sections = @sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')
|
|
218
|
-
"#<Doing::Items #{count} items, #{@sections.count} sections: #{sections}>"
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
end
|
data/lib/doing/prompt.rb
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Doing
|
|
4
|
-
# Terminal Prompt methods
|
|
5
|
-
module Prompt
|
|
6
|
-
class << self
|
|
7
|
-
attr_writer :force_answer, :default_answer
|
|
8
|
-
|
|
9
|
-
include Color
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
## Clear the terminal screen
|
|
13
|
-
##
|
|
14
|
-
def clear_screen(msg = nil)
|
|
15
|
-
puts "\e[H\e[2J" if STDOUT.tty?
|
|
16
|
-
puts msg if msg.good?
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def force_answer
|
|
20
|
-
@force_answer ||= nil
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def default_answer
|
|
24
|
-
@default_answer ||= false
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def enter_text(prompt, default_response: '')
|
|
28
|
-
$stdin.reopen('/dev/tty')
|
|
29
|
-
return default_response if @default_answer
|
|
30
|
-
|
|
31
|
-
print "#{yellow(prompt).sub(/:?$/, ':')} #{reset}"
|
|
32
|
-
$stdin.gets.strip
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def read_line(prompt: 'Enter text', completions: [], default_response: '')
|
|
36
|
-
$stdin.reopen('/dev/tty')
|
|
37
|
-
return default_response if @default_answer
|
|
38
|
-
|
|
39
|
-
unless completions.empty?
|
|
40
|
-
completions.sort!
|
|
41
|
-
comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
|
|
42
|
-
Readline.completion_append_character = ' '
|
|
43
|
-
Readline.completion_proc = comp
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
begin
|
|
47
|
-
Readline.readline("#{yellow(prompt).sub(/:?$/, ':')} #{reset}", true).strip
|
|
48
|
-
rescue Interrupt
|
|
49
|
-
raise UserCancelled
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def read_lines(prompt: 'Enter text', completions: [], default_response: '')
|
|
54
|
-
$stdin.reopen('/dev/tty')
|
|
55
|
-
return default_response if @default_answer
|
|
56
|
-
|
|
57
|
-
completions.sort!
|
|
58
|
-
comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
|
|
59
|
-
Readline.completion_append_character = ' '
|
|
60
|
-
Readline.completion_proc = comp
|
|
61
|
-
puts format(['%<promptcolor>s%<prompt>s %<textcolor>sEnter a blank line',
|
|
62
|
-
'(%<keycolor>sreturn twice%<textcolor>s)',
|
|
63
|
-
'to end editing and save,',
|
|
64
|
-
'%<keycolor>sCTRL-C%<textcolor>s to cancel%<reset>s'].join(' '),
|
|
65
|
-
{ promptcolor: boldgreen, prompt: prompt.sub(/:?$/, ':'),
|
|
66
|
-
textcolor: yellow, keycolor: boldwhite, reset: reset })
|
|
67
|
-
|
|
68
|
-
res = []
|
|
69
|
-
|
|
70
|
-
begin
|
|
71
|
-
while (line = Readline.readline('> ', true))
|
|
72
|
-
break if line.strip.empty?
|
|
73
|
-
|
|
74
|
-
res << line.chomp
|
|
75
|
-
end
|
|
76
|
-
rescue Interrupt
|
|
77
|
-
raise UserCancelled
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
res.join("\n").strip
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def request_lines(prompt: 'Enter text', default_response: '')
|
|
84
|
-
$stdin.reopen('/dev/tty')
|
|
85
|
-
return default_response if @default_answer
|
|
86
|
-
|
|
87
|
-
ask_note = []
|
|
88
|
-
reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
|
|
89
|
-
puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
|
|
90
|
-
loop do
|
|
91
|
-
res = reader.read_line(green('> '))
|
|
92
|
-
break if res.strip.empty?
|
|
93
|
-
|
|
94
|
-
ask_note.push(res)
|
|
95
|
-
end
|
|
96
|
-
ask_note.join("\n").strip
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
##
|
|
100
|
-
## Ask a yes or no question in the terminal
|
|
101
|
-
##
|
|
102
|
-
## @param question [String] The question
|
|
103
|
-
## to ask
|
|
104
|
-
## @param default_response (Bool) default
|
|
105
|
-
## response if no input
|
|
106
|
-
##
|
|
107
|
-
## @return (Bool) yes or no
|
|
108
|
-
##
|
|
109
|
-
def yn(question, default_response: false)
|
|
110
|
-
return @force_answer == :yes ? true : false unless @force_answer.nil?
|
|
111
|
-
|
|
112
|
-
$stdin.reopen('/dev/tty')
|
|
113
|
-
|
|
114
|
-
default = if default_response.is_a?(String)
|
|
115
|
-
default_response =~ /y/i ? true : false
|
|
116
|
-
else
|
|
117
|
-
default_response
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# if global --default is set, answer default
|
|
121
|
-
return default if @default_answer
|
|
122
|
-
|
|
123
|
-
# if this isn't an interactive shell, answer default
|
|
124
|
-
return default unless $stdout.isatty
|
|
125
|
-
|
|
126
|
-
# clear the buffer
|
|
127
|
-
if ARGV&.length
|
|
128
|
-
ARGV.length.times do
|
|
129
|
-
ARGV.shift
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
system 'stty cbreak'
|
|
133
|
-
|
|
134
|
-
cw = white
|
|
135
|
-
cbw = boldwhite
|
|
136
|
-
cbg = boldgreen
|
|
137
|
-
cd = Color.default
|
|
138
|
-
|
|
139
|
-
options = unless default.nil?
|
|
140
|
-
"#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
|
|
141
|
-
else
|
|
142
|
-
"#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
|
|
143
|
-
end
|
|
144
|
-
$stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
|
|
145
|
-
res = $stdin.sysread 1
|
|
146
|
-
puts
|
|
147
|
-
system 'stty cooked'
|
|
148
|
-
|
|
149
|
-
res.chomp!
|
|
150
|
-
res.downcase!
|
|
151
|
-
|
|
152
|
-
return default if res.empty?
|
|
153
|
-
|
|
154
|
-
res =~ /y/i ? true : false
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def fzf
|
|
158
|
-
@fzf ||= install_fzf
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def uninstall_fzf
|
|
162
|
-
fzf_bin = File.join(File.dirname(__FILE__), '../helpers/fzf/bin/fzf')
|
|
163
|
-
FileUtils.rm_f(fzf_bin) if File.exist?(fzf_bin)
|
|
164
|
-
Doing.logger.warn('fzf:', "removed #{fzf_bin}")
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def which_fzf
|
|
168
|
-
fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
|
|
169
|
-
fzf_bin = File.join(fzf_dir, 'bin/fzf')
|
|
170
|
-
return fzf_bin if File.exist?(fzf_bin)
|
|
171
|
-
|
|
172
|
-
Doing.logger.debug('fzf:', 'Using user-installed fzf')
|
|
173
|
-
TTY::Which.which('fzf')
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def silence_std(file = '/dev/null')
|
|
177
|
-
$stdout = File.new(file, 'w')
|
|
178
|
-
$stderr = File.new(file, 'w')
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def restore_std
|
|
182
|
-
$stdout = STDOUT
|
|
183
|
-
$stderr = STDERR
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def install_fzf(force: false)
|
|
187
|
-
if force
|
|
188
|
-
uninstall_fzf
|
|
189
|
-
elsif which_fzf
|
|
190
|
-
return which_fzf
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
|
|
194
|
-
FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
|
|
195
|
-
fzf_bin = File.join(fzf_dir, 'bin/fzf')
|
|
196
|
-
return fzf_bin if File.exist?(fzf_bin)
|
|
197
|
-
|
|
198
|
-
prev_level = Doing.logger.level
|
|
199
|
-
Doing.logger.adjust_verbosity({ log_level: :info })
|
|
200
|
-
Doing.logger.log_now(:warn, 'fzf:', 'Compiling and installing fzf -- this will only happen once')
|
|
201
|
-
Doing.logger.log_now(:warn, 'fzf:', 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
|
|
202
|
-
|
|
203
|
-
silence_std
|
|
204
|
-
`'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null`
|
|
205
|
-
unless File.exist?(fzf_bin)
|
|
206
|
-
restore_std
|
|
207
|
-
Doing.logger.log_now(:warn, 'Error installing, trying again as root')
|
|
208
|
-
silence_std
|
|
209
|
-
`sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null`
|
|
210
|
-
end
|
|
211
|
-
restore_std
|
|
212
|
-
unless File.exist?(fzf_bin)
|
|
213
|
-
Doing.logger.error('fzf:', 'unable to install fzf. You can install manually and Doing will use the system version.')
|
|
214
|
-
Doing.logger.error('fzf:', 'see https://github.com/junegunn/fzf#installation')
|
|
215
|
-
raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues')
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
Doing.logger.info('fzf:', "installed to #{fzf}")
|
|
219
|
-
Doing.logger.adjust_verbosity({ log_level: prev_level })
|
|
220
|
-
fzf_bin
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
##
|
|
224
|
-
## Generate a menu of options and allow user selection
|
|
225
|
-
##
|
|
226
|
-
## @return [String] The selected option
|
|
227
|
-
##
|
|
228
|
-
def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
|
|
229
|
-
return nil unless $stdout.isatty
|
|
230
|
-
|
|
231
|
-
# fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
|
|
232
|
-
default_args = []
|
|
233
|
-
default_args << %(--prompt="#{prompt}")
|
|
234
|
-
default_args << "--height=#{options.count + 2}"
|
|
235
|
-
default_args << '--info=inline'
|
|
236
|
-
default_args << '--multi' if multiple
|
|
237
|
-
header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
|
|
238
|
-
default_args << %(--header="#{header}")
|
|
239
|
-
default_args.concat(fzf_args)
|
|
240
|
-
options.sort! if sorted
|
|
241
|
-
|
|
242
|
-
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{default_args.join(' ')}`
|
|
243
|
-
return false if res.strip.size.zero?
|
|
244
|
-
|
|
245
|
-
res
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
##
|
|
249
|
-
## Create an interactive menu to select from a set of Items
|
|
250
|
-
##
|
|
251
|
-
## @param items [Array] list of items
|
|
252
|
-
## @param opt Additional options
|
|
253
|
-
##
|
|
254
|
-
## @option opt [Boolean] :include_section Include section name for each item in menu
|
|
255
|
-
## @option opt [String] :header A custom header string
|
|
256
|
-
## @option opt [String] :prompt A custom prompt string
|
|
257
|
-
## @option opt [String] :query Initial query
|
|
258
|
-
## @option opt [Boolean] :show_if_single Show menu even if there's only one option
|
|
259
|
-
## @option opt [Boolean] :menu Show menu
|
|
260
|
-
## @option opt [Boolean] :sort Sort options
|
|
261
|
-
## @option opt [Boolean] :multiple Allow multiple selections
|
|
262
|
-
## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
|
|
263
|
-
##
|
|
264
|
-
def choose_from_items(items, **opt)
|
|
265
|
-
return items unless $stdout.isatty
|
|
266
|
-
|
|
267
|
-
return nil unless items.count.positive?
|
|
268
|
-
|
|
269
|
-
case_sensitive = opt.fetch(:case, :smart).normalize_case
|
|
270
|
-
header = opt.fetch(:header, 'Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit')
|
|
271
|
-
prompt = opt.fetch(:prompt, 'Select entries to act on > ')
|
|
272
|
-
query = opt.fetch(:query) { opt.fetch(:search, '') }
|
|
273
|
-
include_section = opt.fetch(:include_section, false)
|
|
274
|
-
|
|
275
|
-
pad = items.length.to_s.length
|
|
276
|
-
options = items.map.with_index do |item, i|
|
|
277
|
-
out = [
|
|
278
|
-
format("%#{pad}d", i),
|
|
279
|
-
') ',
|
|
280
|
-
format('%16s', item.date.strftime('%Y-%m-%d %H:%M')),
|
|
281
|
-
' | ',
|
|
282
|
-
item.title
|
|
283
|
-
]
|
|
284
|
-
if include_section
|
|
285
|
-
out.concat([
|
|
286
|
-
' (',
|
|
287
|
-
item.section,
|
|
288
|
-
') '
|
|
289
|
-
])
|
|
290
|
-
end
|
|
291
|
-
out.join('')
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
fzf_args = [
|
|
295
|
-
%(--header="#{header}"),
|
|
296
|
-
%(--prompt="#{prompt.sub(/ *$/, ' ')}"),
|
|
297
|
-
opt.fetch(:multiple) ? '--multi' : '--no-multi',
|
|
298
|
-
'-0',
|
|
299
|
-
'--bind ctrl-a:select-all',
|
|
300
|
-
%(-q "#{query}"),
|
|
301
|
-
'--info=inline'
|
|
302
|
-
]
|
|
303
|
-
fzf_args.push('-1') unless opt.fetch(:show_if_single, false)
|
|
304
|
-
fzf_args << case case_sensitive
|
|
305
|
-
when :sensitive
|
|
306
|
-
'+i'
|
|
307
|
-
when :ignore
|
|
308
|
-
'-i'
|
|
309
|
-
end
|
|
310
|
-
fzf_args << '-e' if opt.fetch(:exact, false)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
unless opt.fetch(:menu)
|
|
314
|
-
raise InvalidArgument, "Can't skip menu when no query is provided" unless query && !query.empty?
|
|
315
|
-
|
|
316
|
-
fzf_args.concat([%(--filter="#{query}"), opt.fetch(:sort) ? '' : '--no-sort'])
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
|
|
320
|
-
selected = []
|
|
321
|
-
res.split(/\n/).each do |item|
|
|
322
|
-
idx = item.match(/^ *(\d+)\)/)[1].to_i
|
|
323
|
-
selected.push(items[idx])
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
opt.fetch(:multiple) ? selected : selected[0]
|
|
327
|
-
end
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
end
|