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,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
class WWID
|
|
5
|
+
##
|
|
6
|
+
## Create a process for an editor and wait for the file handle to return
|
|
7
|
+
##
|
|
8
|
+
## @param input [String] Text input for editor
|
|
9
|
+
##
|
|
10
|
+
def fork_editor(input = '', message: :default)
|
|
11
|
+
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
|
12
|
+
|
|
13
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
|
|
14
|
+
|
|
15
|
+
tmpfile = Tempfile.new(['doing_temp', '.doing'])
|
|
16
|
+
|
|
17
|
+
File.open(tmpfile.path, 'w+') do |f|
|
|
18
|
+
f.puts input
|
|
19
|
+
unless message.nil?
|
|
20
|
+
f.puts message == :default ? '# First line is the entry title, lines after are added as a note' : message
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
pid = Process.fork { system("#{Util.editor_with_args} #{tmpfile.path}") }
|
|
25
|
+
|
|
26
|
+
trap('INT') do
|
|
27
|
+
begin
|
|
28
|
+
Process.kill(9, pid)
|
|
29
|
+
rescue StandardError
|
|
30
|
+
Errno::ESRCH
|
|
31
|
+
end
|
|
32
|
+
tmpfile.unlink
|
|
33
|
+
tmpfile.close!
|
|
34
|
+
exit 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Process.wait(pid)
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
if $?.exitstatus == 0
|
|
41
|
+
input = IO.read(tmpfile.path)
|
|
42
|
+
else
|
|
43
|
+
exit_now! 'Cancelled'
|
|
44
|
+
end
|
|
45
|
+
ensure
|
|
46
|
+
tmpfile.close
|
|
47
|
+
tmpfile.unlink
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
input.split(/\n/).delete_if(&:ignore?).join("\n")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
## Takes a multi-line string and formats it as an entry
|
|
55
|
+
##
|
|
56
|
+
## @param input [String] The string to parse
|
|
57
|
+
##
|
|
58
|
+
## @return [Array] [[String]title, [Note]note]
|
|
59
|
+
##
|
|
60
|
+
def format_input(input)
|
|
61
|
+
raise EmptyInput, 'No content in entry' if input.nil? || input.strip.empty?
|
|
62
|
+
|
|
63
|
+
input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
|
|
64
|
+
title = input_lines[0]&.strip
|
|
65
|
+
raise EmptyInput, 'No content in first line' if title.nil? || title.strip.empty?
|
|
66
|
+
|
|
67
|
+
date = nil
|
|
68
|
+
iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
|
|
69
|
+
date_rx = /^(?:\s*- )?(?<date>.*?) \| (?=\S)/
|
|
70
|
+
|
|
71
|
+
raise EmptyInput, 'No content' if title.sub(/^.*?\| */, '').strip.empty?
|
|
72
|
+
|
|
73
|
+
title.expand_date_tags(Doing.setting('date_tags'))
|
|
74
|
+
|
|
75
|
+
if title =~ date_rx
|
|
76
|
+
m = title.match(date_rx)
|
|
77
|
+
d = m['date']
|
|
78
|
+
date = if d =~ iso_rx
|
|
79
|
+
Time.parse(d)
|
|
80
|
+
else
|
|
81
|
+
d.chronify(guess: :begin)
|
|
82
|
+
end
|
|
83
|
+
title.sub!(date_rx, '').strip!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
note = Note.new
|
|
87
|
+
note.add(input_lines[1..-1]) if input_lines.length > 1
|
|
88
|
+
# If title line ends in a parenthetical, use that as the note
|
|
89
|
+
if note.empty? && title =~ /\s+\(.*?\)$/
|
|
90
|
+
title.sub!(/\s+\((?<note>.*?)\)$/) do
|
|
91
|
+
m = Regexp.last_match
|
|
92
|
+
note.add(m['note'])
|
|
93
|
+
''
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
note.strip_lines!
|
|
98
|
+
note.compress
|
|
99
|
+
|
|
100
|
+
[date, title, note]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def add_with_editor(**options)
|
|
104
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Util.default_editor.nil?
|
|
105
|
+
|
|
106
|
+
input = options[:date].strftime('%F %R | ')
|
|
107
|
+
input += options[:title]
|
|
108
|
+
input += "\n#{options[:note]}" if options[:note]
|
|
109
|
+
input = fork_editor(input).strip
|
|
110
|
+
|
|
111
|
+
d, title, note = format_input(input)
|
|
112
|
+
raise EmptyInput, 'No content' if title.empty?
|
|
113
|
+
|
|
114
|
+
if options[:ask]
|
|
115
|
+
ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
|
|
116
|
+
note.add(ask_note) unless ask_note.empty?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
date = d.nil? ? options[:date] : d
|
|
120
|
+
finish = options[:finish_last] || false
|
|
121
|
+
add_item(title.cap_first, options[:section], { note: note, back: date, timed: finish })
|
|
122
|
+
write(@doing_file)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def edit_items(items)
|
|
126
|
+
items.sort_by! { |i| i.date }
|
|
127
|
+
editable_items = []
|
|
128
|
+
|
|
129
|
+
items.each do |i|
|
|
130
|
+
editable = "#{i.date.strftime('%F %R')} | #{i.title}"
|
|
131
|
+
old_note = i.note ? i.note.strip_lines.join("\n") : nil
|
|
132
|
+
editable += "\n#{old_note}" unless old_note.nil?
|
|
133
|
+
editable_items << editable
|
|
134
|
+
end
|
|
135
|
+
divider = "-----------"
|
|
136
|
+
notice =<<~EONOTICE
|
|
137
|
+
|
|
138
|
+
# - You may delete entries, but leave all divider lines (---) in place.
|
|
139
|
+
# - Start and @done dates replaced with a time string (yesterday 3pm) will
|
|
140
|
+
# be parsed automatically. Do not delete the pipe (|) between start date
|
|
141
|
+
# and entry title.
|
|
142
|
+
EONOTICE
|
|
143
|
+
input = "#{editable_items.map(&:strip).join("\n#{divider}\n")}\n"
|
|
144
|
+
|
|
145
|
+
new_items = fork_editor(input, message: notice).split(/^#{divider}/).map(&:strip)
|
|
146
|
+
|
|
147
|
+
new_items.each_with_index do |new_item, i|
|
|
148
|
+
input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
|
|
149
|
+
first_line = input_lines[0]&.strip
|
|
150
|
+
|
|
151
|
+
if first_line.nil? || first_line =~ /^#{divider.strip}$/ || first_line.strip.empty?
|
|
152
|
+
deleted = @content.delete_item(items[i], single: new_items.count == 1)
|
|
153
|
+
Hooks.trigger :post_entry_removed, self, deleted
|
|
154
|
+
Doing.logger.info('Deleted:', deleted.title)
|
|
155
|
+
else
|
|
156
|
+
date, title, note = format_input(new_item)
|
|
157
|
+
|
|
158
|
+
note.map!(&:strip)
|
|
159
|
+
note.delete_if(&:ignore?)
|
|
160
|
+
item = items[i]
|
|
161
|
+
old_item = item.clone
|
|
162
|
+
item.date = date || items[i].date
|
|
163
|
+
item.title = title
|
|
164
|
+
item.note = note
|
|
165
|
+
if (item.equal?(old_item))
|
|
166
|
+
Doing.logger.count(:skipped, level: :debug)
|
|
167
|
+
else
|
|
168
|
+
Doing.logger.count(:updated)
|
|
169
|
+
Hooks.trigger :post_entry_updated, self, item, old_item
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
##
|
|
176
|
+
## Edit the last entry
|
|
177
|
+
##
|
|
178
|
+
## @param section [String] The section, default "All"
|
|
179
|
+
##
|
|
180
|
+
def edit_last(section: 'All', options: {})
|
|
181
|
+
options[:section] = guess_section(section)
|
|
182
|
+
|
|
183
|
+
item = last_entry(options)
|
|
184
|
+
|
|
185
|
+
if item.nil?
|
|
186
|
+
logger.debug('Skipped:', 'No entries found')
|
|
187
|
+
return
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
old_item = item.clone
|
|
191
|
+
content = ["#{item.date.strftime('%F %R')} | #{item.title.dup}"]
|
|
192
|
+
content << item.note.strip_lines.join("\n") unless item.note.empty?
|
|
193
|
+
new_item = fork_editor(content.join("\n"))
|
|
194
|
+
raise UserCancelled, 'No change' if new_item.strip == content.join("\n").strip
|
|
195
|
+
|
|
196
|
+
date, title, note = format_input(new_item)
|
|
197
|
+
date ||= item.date
|
|
198
|
+
|
|
199
|
+
if title.nil? || title.empty?
|
|
200
|
+
logger.debug('Skipped:', 'No content provided')
|
|
201
|
+
elsif title == item.title && note.equal?(item.note) && date.equal?(item.date)
|
|
202
|
+
logger.debug('Skipped:', 'No change in content')
|
|
203
|
+
else
|
|
204
|
+
item.date = date unless date.nil?
|
|
205
|
+
item.title = title
|
|
206
|
+
item.note.add(note, replace: true)
|
|
207
|
+
logger.info('Edited:', item.title)
|
|
208
|
+
Hooks.trigger :post_entry_updated, self, item, old_item
|
|
209
|
+
|
|
210
|
+
write(@doing_file)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
class WWID
|
|
5
|
+
##
|
|
6
|
+
## Initializes the doing file.
|
|
7
|
+
##
|
|
8
|
+
## @param path [String] Override path to a doing file, optional
|
|
9
|
+
##
|
|
10
|
+
def init_doing_file(path = nil)
|
|
11
|
+
@doing_file = File.expand_path(Doing.setting('doing_file'))
|
|
12
|
+
|
|
13
|
+
if path.nil?
|
|
14
|
+
create(@doing_file) unless File.exist?(@doing_file)
|
|
15
|
+
input = IO.read(@doing_file)
|
|
16
|
+
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
|
17
|
+
logger.debug('Read:', "read file #{@doing_file}")
|
|
18
|
+
elsif File.exist?(File.expand_path(path)) && File.file?(File.expand_path(path)) && File.stat(File.expand_path(path)).size.positive?
|
|
19
|
+
@doing_file = File.expand_path(path)
|
|
20
|
+
input = IO.read(File.expand_path(path))
|
|
21
|
+
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
|
22
|
+
logger.debug('Read:', "read file #{File.expand_path(path)}")
|
|
23
|
+
elsif path.length < 256
|
|
24
|
+
@doing_file = File.expand_path(path)
|
|
25
|
+
create(path)
|
|
26
|
+
input = IO.read(File.expand_path(path))
|
|
27
|
+
input = input.force_encoding('utf-8') if input.respond_to? :force_encoding
|
|
28
|
+
logger.debug('Read:', "read file #{File.expand_path(path)}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@other_content_top = []
|
|
32
|
+
@other_content_bottom = []
|
|
33
|
+
|
|
34
|
+
section = nil
|
|
35
|
+
lines = input.split(/[\n\r]/)
|
|
36
|
+
|
|
37
|
+
lines.each do |line|
|
|
38
|
+
next if line =~ /^\s*$/
|
|
39
|
+
|
|
40
|
+
if line =~ /^(\S[\S ]+):\s*(@[\w\-_.]+\s*)*$/
|
|
41
|
+
section = Regexp.last_match(1)
|
|
42
|
+
@content.add_section(Section.new(section, original: line), log: false)
|
|
43
|
+
elsif line =~ /^\s*- (\d{4}-\d\d-\d\d \d\d:\d\d) \| (.*)/
|
|
44
|
+
if section.nil?
|
|
45
|
+
section = 'Uncategorized'
|
|
46
|
+
@content.add_section(Section.new(section, original: 'Uncategorized:'), log: false)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
date = Regexp.last_match(1).strip
|
|
50
|
+
title = Regexp.last_match(2).strip
|
|
51
|
+
item = Item.new(date, title, section)
|
|
52
|
+
@content.push(item)
|
|
53
|
+
elsif @content.count.zero?
|
|
54
|
+
# if content[section].items.length - 1 == current
|
|
55
|
+
@other_content_top.push(line)
|
|
56
|
+
elsif line =~ /^\S/
|
|
57
|
+
@other_content_bottom.push(line)
|
|
58
|
+
else
|
|
59
|
+
prev_item = @content.last
|
|
60
|
+
prev_item.note = Note.new unless prev_item.note
|
|
61
|
+
|
|
62
|
+
prev_item.note.add(line)
|
|
63
|
+
# end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
Hooks.trigger :post_read, self
|
|
68
|
+
@initial_content = @content.clone
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
## Create a new doing file
|
|
73
|
+
##
|
|
74
|
+
def create(filename = nil)
|
|
75
|
+
filename = @doing_file if filename.nil?
|
|
76
|
+
return if File.exist?(filename) && File.stat(filename).size.positive?
|
|
77
|
+
|
|
78
|
+
FileUtils.mkdir_p(File.dirname(filename)) unless File.directory?(File.dirname(filename))
|
|
79
|
+
|
|
80
|
+
File.open(filename, 'w+') do |f|
|
|
81
|
+
f.puts "#{Doing.setting('current_section')}:"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
## Write content to file or STDOUT
|
|
87
|
+
##
|
|
88
|
+
## @param file [String] The filepath to write to
|
|
89
|
+
##
|
|
90
|
+
def write(file = nil, backup: true)
|
|
91
|
+
Hooks.trigger :pre_write, self, file
|
|
92
|
+
output = combined_content
|
|
93
|
+
if file.nil?
|
|
94
|
+
$stdout.puts output
|
|
95
|
+
else
|
|
96
|
+
Util.write_to_file(file, output, backup: backup)
|
|
97
|
+
run_after if Doing.setting('run_after')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
## Rename doing file with date and start fresh one
|
|
103
|
+
##
|
|
104
|
+
def rotate(opt)
|
|
105
|
+
opt ||= {}
|
|
106
|
+
keep = opt[:keep] || 0
|
|
107
|
+
tags = []
|
|
108
|
+
tags.concat(opt[:tag].split(/ *, */).map { |t| t.sub(/^@/, '').strip }) if opt[:tag]
|
|
109
|
+
bool = opt[:bool] || :and
|
|
110
|
+
sect = opt[:section] !~ /^all$/i ? guess_section(opt[:section]) : 'all'
|
|
111
|
+
|
|
112
|
+
section = guess_section(sect)
|
|
113
|
+
|
|
114
|
+
section_items = @content.in_section(section)
|
|
115
|
+
max = section_items.count - keep.to_i
|
|
116
|
+
|
|
117
|
+
counter = 0
|
|
118
|
+
new_content = Items.new
|
|
119
|
+
|
|
120
|
+
section_items.each do |item|
|
|
121
|
+
break if counter >= max
|
|
122
|
+
if opt[:before]
|
|
123
|
+
time_string = opt[:before]
|
|
124
|
+
cutoff = time_string.chronify(guess: :begin)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
unless ((!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s)) || (opt[:before] && item.date >= cutoff))
|
|
128
|
+
new_item = @content.delete(item)
|
|
129
|
+
Hooks.trigger :post_entry_removed, self, item.clone
|
|
130
|
+
raise DoingRuntimeError, "Error deleting item: #{item}" if new_item.nil?
|
|
131
|
+
|
|
132
|
+
new_content.add_section(new_item.section, log: false)
|
|
133
|
+
new_content.push(new_item)
|
|
134
|
+
counter += 1
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if counter.positive?
|
|
139
|
+
logger.count(:rotated,
|
|
140
|
+
level: :info,
|
|
141
|
+
count: counter,
|
|
142
|
+
message: "Rotated %count %items")
|
|
143
|
+
else
|
|
144
|
+
logger.info('Skipped:', 'No items were rotated')
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
write(@doing_file)
|
|
148
|
+
|
|
149
|
+
file = @doing_file.sub(/(\.\w+)$/, "_#{Time.now.strftime('%Y-%m-%d')}\\1")
|
|
150
|
+
if File.exist?(file)
|
|
151
|
+
init_doing_file(file)
|
|
152
|
+
@content.concat(new_content).uniq!
|
|
153
|
+
logger.warn('File update:', "added entries to existing file: #{file}")
|
|
154
|
+
else
|
|
155
|
+
@content = new_content
|
|
156
|
+
logger.warn('File update:', "created new file: #{file}")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
write(file, backup: false)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
##
|
|
165
|
+
## Wraps doing file content with additional
|
|
166
|
+
## header/footer content
|
|
167
|
+
##
|
|
168
|
+
## @return [String] concatenated content
|
|
169
|
+
## @api private
|
|
170
|
+
def combined_content
|
|
171
|
+
output = @other_content_top ? "#{@other_content_top.join("\n")}\n" : ''
|
|
172
|
+
was_color = Color.coloring?
|
|
173
|
+
Color.coloring = false
|
|
174
|
+
@content.dedup!(match_section: true)
|
|
175
|
+
output += @content.to_s
|
|
176
|
+
output += @other_content_bottom.join("\n") unless @other_content_bottom.nil?
|
|
177
|
+
# Just strip all ANSI colors from the content before writing to doing file
|
|
178
|
+
Color.coloring = was_color
|
|
179
|
+
|
|
180
|
+
output.uncolor
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
class WWID
|
|
5
|
+
# Use fzf to filter an Items object with a search query.
|
|
6
|
+
# Faster than {#filter_items} when all you need is a
|
|
7
|
+
# text search of the title and note
|
|
8
|
+
#
|
|
9
|
+
# @param items [Items] an Items object
|
|
10
|
+
# @param query [String] The search query
|
|
11
|
+
# @param case_type [Symbol] The case type (:smart, :sensitive, :ignore)
|
|
12
|
+
#
|
|
13
|
+
# @return [Items] Filtered Items array
|
|
14
|
+
#
|
|
15
|
+
def fuzzy_filter_items(items, query, case_type: :smart)
|
|
16
|
+
scannable = items.map.with_index { |item, idx| "#{item.title} #{item.note.join(' ')}".gsub(/[|*?!]/, '') + "|#{idx}" }.join("\n")
|
|
17
|
+
|
|
18
|
+
fzf_args = [
|
|
19
|
+
'--multi',
|
|
20
|
+
%(--filter="#{query.sub(/^'?/, "'")}"),
|
|
21
|
+
'--no-sort',
|
|
22
|
+
'-d "\|"',
|
|
23
|
+
'--nth=1'
|
|
24
|
+
]
|
|
25
|
+
fzf_args << case case_type.normalize_case
|
|
26
|
+
when :smart
|
|
27
|
+
query =~ /[A-Z]/ ? '+i' : '-i'
|
|
28
|
+
when :sensitive
|
|
29
|
+
'+i'
|
|
30
|
+
when :ignore
|
|
31
|
+
'-i'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# fzf_args << '-e' if opt[:exact]
|
|
35
|
+
# puts fzf_args.join(' ')
|
|
36
|
+
res = `echo #{Shellwords.escape(scannable)}|#{Prompt.fzf} #{fzf_args.join(' ')}`
|
|
37
|
+
selected = Items.new
|
|
38
|
+
res.split(/\n/).each do |item|
|
|
39
|
+
idx = item.match(/\|(\d+)$/)[1].to_i
|
|
40
|
+
selected.push(items[idx])
|
|
41
|
+
end
|
|
42
|
+
selected
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
## Filter items based on search criteria
|
|
47
|
+
##
|
|
48
|
+
## @param items [Array] The items to filter (if empty, filters all items)
|
|
49
|
+
## @param opt [Hash] The filter parameters
|
|
50
|
+
##
|
|
51
|
+
## @option opt [String] :section ('all')
|
|
52
|
+
## @option opt [Boolean] :unfinished (false)
|
|
53
|
+
## @option opt [Array or String] :tag ([]) Array or comma-separated string
|
|
54
|
+
## @option opt [Symbol] :tag_bool (:and) :and, :or, :not
|
|
55
|
+
## @option opt [String] :search ('') string, optional regex with `/string/`
|
|
56
|
+
## @option opt [Array] :date_filter (nil) [[Time]start, [Time]end]
|
|
57
|
+
## @option opt [Boolean] :only_timed (false)
|
|
58
|
+
## @option opt [String] :before (nil) Date/Time string, unparsed
|
|
59
|
+
## @option opt [String] :after (nil) Date/Time string, unparsed
|
|
60
|
+
## @option opt [Boolean] :today (false) limit to entries from today
|
|
61
|
+
## @option opt [Boolean] :yesterday (false) limit to entries from yesterday
|
|
62
|
+
## @option opt [Number] :count (0) max entries to return
|
|
63
|
+
## @option opt [String] :age (new) 'old' or 'new'
|
|
64
|
+
## @option opt [Array] :val (nil) Array of tag value queries
|
|
65
|
+
##
|
|
66
|
+
def filter_items(items = Items.new, opt: {})
|
|
67
|
+
logger.benchmark(:filter_items, :start)
|
|
68
|
+
time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/i
|
|
69
|
+
|
|
70
|
+
if items.nil? || items.empty?
|
|
71
|
+
section = opt[:section] ? guess_section(opt[:section]) : 'All'
|
|
72
|
+
items = section =~ /^all$/i ? @content.clone : @content.in_section(section)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless opt[:time_filter]
|
|
76
|
+
opt[:time_filter] = [nil, nil]
|
|
77
|
+
if opt[:from] && !opt[:date_filter]
|
|
78
|
+
if opt[:from][0].is_a?(String) && opt[:from][0] =~ time_rx
|
|
79
|
+
opt[:time_filter] = opt[:from]
|
|
80
|
+
elsif opt[:from][0].is_a?(Time)
|
|
81
|
+
opt[:date_filter] = opt[:from]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if opt[:before].is_a?(String) && opt[:before] =~ time_rx
|
|
87
|
+
opt[:time_filter][1] = opt[:before]
|
|
88
|
+
opt[:before] = nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if opt[:after].is_a?(String) && opt[:after] =~ time_rx
|
|
92
|
+
opt[:time_filter][0] = opt[:after]
|
|
93
|
+
opt[:after] = nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
items.sort_by! { |item| [item.date, item.title.downcase] }.reverse
|
|
97
|
+
|
|
98
|
+
filtered_items = items.select do |item|
|
|
99
|
+
keep = true
|
|
100
|
+
if opt[:unfinished]
|
|
101
|
+
finished = item.tags?('done', :and)
|
|
102
|
+
finished = opt[:not] ? !finished : finished
|
|
103
|
+
keep = false if finished
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if keep && opt[:val]&.count&.positive?
|
|
107
|
+
bool = opt[:bool].normalize_bool if opt[:bool]
|
|
108
|
+
bool ||= :and
|
|
109
|
+
bool = :and if bool == :pattern
|
|
110
|
+
|
|
111
|
+
val_match = opt[:val].nil? || opt[:val].empty? ? true : item.tag_values?(opt[:val], bool)
|
|
112
|
+
keep = false unless val_match
|
|
113
|
+
keep = opt[:not] ? !keep : keep
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if keep && opt[:tag]
|
|
117
|
+
opt[:tag_bool] = opt[:bool].normalize_bool if opt[:bool]
|
|
118
|
+
opt[:tag_bool] ||= :and
|
|
119
|
+
tag_match = opt[:tag].nil? || opt[:tag].empty? ? true : item.tags?(opt[:tag], opt[:tag_bool])
|
|
120
|
+
keep = false unless tag_match
|
|
121
|
+
keep = opt[:not] ? !keep : keep
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if keep && opt[:search]
|
|
125
|
+
search_match = if opt[:search].nil? || opt[:search].empty?
|
|
126
|
+
true
|
|
127
|
+
else
|
|
128
|
+
item.search(opt[:search], case_type: opt[:case].normalize_case)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
keep = false unless search_match
|
|
132
|
+
keep = opt[:not] ? !keep : keep
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if keep && opt[:date_filter]&.length == 2
|
|
136
|
+
start_date = opt[:date_filter][0]
|
|
137
|
+
end_date = opt[:date_filter][1]
|
|
138
|
+
|
|
139
|
+
in_date_range = if end_date
|
|
140
|
+
item.date >= start_date && item.date <= end_date
|
|
141
|
+
else
|
|
142
|
+
item.date.strftime('%F') == start_date.strftime('%F')
|
|
143
|
+
end
|
|
144
|
+
keep = false unless in_date_range
|
|
145
|
+
keep = opt[:not] ? !keep : keep
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
if keep && opt[:time_filter][0] || opt[:time_filter][1]
|
|
149
|
+
start_string = if opt[:time_filter][0].nil?
|
|
150
|
+
"#{item.date.strftime('%Y-%m-%d')} 12am"
|
|
151
|
+
else
|
|
152
|
+
"#{item.date.strftime('%Y-%m-%d')} #{opt[:time_filter][0]}"
|
|
153
|
+
end
|
|
154
|
+
start_time = start_string.chronify(guess: :begin)
|
|
155
|
+
|
|
156
|
+
end_string = if opt[:time_filter][1].nil?
|
|
157
|
+
"#{item.date.to_datetime.next_day.strftime('%Y-%m-%d')} 12am"
|
|
158
|
+
else
|
|
159
|
+
"#{item.date.strftime('%Y-%m-%d')} #{opt[:time_filter][1]}"
|
|
160
|
+
end
|
|
161
|
+
end_time = end_string.chronify(guess: :end)
|
|
162
|
+
|
|
163
|
+
in_time_range = item.date >= start_time && item.date <= end_time
|
|
164
|
+
keep = false unless in_time_range
|
|
165
|
+
keep = opt[:not] ? !keep : keep
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
keep = false if keep && opt[:only_timed] && !item.interval
|
|
169
|
+
|
|
170
|
+
if keep && opt[:tag_filter]
|
|
171
|
+
keep = item.tags?(opt[:tag_filter]['tags'], opt[:tag_filter]['bool'])
|
|
172
|
+
keep = opt[:not] ? !keep : keep
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if keep && opt[:before]
|
|
176
|
+
before = opt[:before]
|
|
177
|
+
cutoff = if before =~ time_rx
|
|
178
|
+
"#{item.date.strftime('%Y-%m-%d')} #{before}".chronify(guess: :begin)
|
|
179
|
+
elsif before.is_a?(String)
|
|
180
|
+
before.chronify(guess: :begin)
|
|
181
|
+
else
|
|
182
|
+
before
|
|
183
|
+
end
|
|
184
|
+
keep = cutoff && item.date <= cutoff
|
|
185
|
+
keep = opt[:not] ? !keep : keep
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if keep && opt[:after]
|
|
189
|
+
after = opt[:after]
|
|
190
|
+
cutoff = if after =~ time_rx
|
|
191
|
+
"#{item.date.strftime('%Y-%m-%d')} #{after}".chronify(guess: :end)
|
|
192
|
+
elsif after.is_a?(String)
|
|
193
|
+
after.chronify(guess: :end)
|
|
194
|
+
else
|
|
195
|
+
after
|
|
196
|
+
end
|
|
197
|
+
keep = cutoff && item.date >= cutoff
|
|
198
|
+
keep = opt[:not] ? !keep : keep
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if keep && opt[:today]
|
|
202
|
+
keep = item.date >= Date.today.to_time && item.date < Date.today.next_day.to_time
|
|
203
|
+
keep = opt[:not] ? !keep : keep
|
|
204
|
+
elsif keep && opt[:yesterday]
|
|
205
|
+
keep = item.date >= Date.today.prev_day.to_time && item.date < Date.today.to_time
|
|
206
|
+
keep = opt[:not] ? !keep : keep
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
keep
|
|
210
|
+
end
|
|
211
|
+
count = opt[:count].to_i&.positive? ? opt[:count].to_i : filtered_items.count
|
|
212
|
+
|
|
213
|
+
output = Items.new
|
|
214
|
+
|
|
215
|
+
if opt[:age] && opt[:age].normalize_age == :oldest
|
|
216
|
+
output.concat(filtered_items.slice(0, count).reverse)
|
|
217
|
+
else
|
|
218
|
+
output.concat(filtered_items.reverse.slice(0, count))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
logger.benchmark(:filter_items, :finish)
|
|
222
|
+
|
|
223
|
+
output
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doing
|
|
4
|
+
class WWID
|
|
5
|
+
##
|
|
6
|
+
## Attempt to match a string with an existing section
|
|
7
|
+
##
|
|
8
|
+
## @param frag [String] The user-provided string
|
|
9
|
+
## @param guessed [Boolean] already guessed and failed
|
|
10
|
+
##
|
|
11
|
+
def guess_section(frag, guessed: false, suggest: false)
|
|
12
|
+
return 'All' if frag =~ /^all$/i
|
|
13
|
+
|
|
14
|
+
frag ||= Doing.setting('current_section')
|
|
15
|
+
|
|
16
|
+
return frag.cap_first if @content.section?(frag)
|
|
17
|
+
|
|
18
|
+
found = @content.guess_section(frag, distance: 2)
|
|
19
|
+
|
|
20
|
+
section = found ? found.title : nil
|
|
21
|
+
|
|
22
|
+
return section if suggest
|
|
23
|
+
|
|
24
|
+
unless section || guessed
|
|
25
|
+
alt = guess_view(frag, guessed: true, suggest: true)
|
|
26
|
+
if alt
|
|
27
|
+
prompt = Color.template("{bw}Did you mean `{xy}doing {by}view {xy}#{alt}`{bw}?{x}")
|
|
28
|
+
meant_view = Prompt.yn(prompt, default_response: 'n')
|
|
29
|
+
|
|
30
|
+
msg = format('%<y>srun with `%<w>sdoing view %<alt>s%<y>s`', w: boldwhite, y: yellow, alt: alt)
|
|
31
|
+
raise Errors::WrongCommand.new(msg, topic: 'Try again:') if meant_view
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
res = Prompt.yn("#{boldwhite}Section #{frag.yellow}#{boldwhite} not found, create it", default_response: 'n')
|
|
36
|
+
|
|
37
|
+
if res
|
|
38
|
+
@content.add_section(frag.cap_first, log: true)
|
|
39
|
+
write(@doing_file)
|
|
40
|
+
return frag.cap_first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
raise Errors::InvalidSection.new("unknown section #{frag.bold.white}", topic: 'Missing:')
|
|
44
|
+
end
|
|
45
|
+
section ? section.cap_first : guessed
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
## Attempt to match a string with an existing view
|
|
50
|
+
##
|
|
51
|
+
## @param frag [String] The user-provided string
|
|
52
|
+
## @param guessed [Boolean] already guessed
|
|
53
|
+
##
|
|
54
|
+
def guess_view(frag, guessed: false, suggest: false)
|
|
55
|
+
views.each { |view| return view if frag.downcase == view.downcase }
|
|
56
|
+
view = nil
|
|
57
|
+
re = frag.to_rx(distance: 2, case_type: :ignore)
|
|
58
|
+
views.each do |v|
|
|
59
|
+
next unless v =~ /#{re}/i
|
|
60
|
+
|
|
61
|
+
logger.debug('Match:', %(Assuming "#{v}" from "#{frag}"))
|
|
62
|
+
view = v
|
|
63
|
+
break
|
|
64
|
+
end
|
|
65
|
+
unless view || guessed
|
|
66
|
+
alt = guess_section(frag, guessed: true, suggest: true)
|
|
67
|
+
|
|
68
|
+
raise Errors::InvalidView.new(%(unknown view #{frag.bold.white}), topic: 'Missing:') unless alt
|
|
69
|
+
|
|
70
|
+
prompt = Color.template("{bw}Did you mean `{xy}doing {by}show {xy}#{alt}`{bw}?{x}")
|
|
71
|
+
meant_view = Prompt.yn(prompt, default_response: 'n')
|
|
72
|
+
|
|
73
|
+
if meant_view
|
|
74
|
+
msg = format('%<y>srun with `%<w>sdoing show %<alt>s%<y>s`', w: boldwhite, y: yellow, alt: alt)
|
|
75
|
+
raise Errors::WrongCommand.new(msg, topic: 'Try again:')
|
|
76
|
+
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raise Errors::InvalidView.new(%(unknown view #{alt.bold.white}), topic: 'Missing:')
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
view
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|