doing 2.1.30 → 2.1.34
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.irbrc +1 -0
- data/CHANGELOG.md +4972 -0
- data/Dockerfile-2.6 +3 -1
- data/Dockerfile-2.7 +4 -2
- data/Dockerfile-3.0 +3 -1
- data/Gemfile.lock +1 -67
- data/README.md +1 -1
- data/bash_profile +13 -0
- data/bin/commands/again.rb +1 -1
- data/bin/commands/archive.rb +3 -3
- data/bin/commands/cancel.rb +1 -1
- data/bin/commands/commands.rb +8 -8
- data/bin/commands/completion.rb +61 -19
- data/bin/commands/config.rb +22 -19
- data/bin/commands/done.rb +2 -2
- data/bin/commands/flag.rb +1 -1
- data/bin/commands/grep.rb +6 -33
- data/bin/commands/last.rb +1 -1
- data/bin/commands/meanwhile.rb +2 -2
- data/bin/commands/now.rb +2 -2
- data/bin/commands/on.rb +6 -16
- data/bin/commands/open.rb +1 -1
- data/bin/commands/recent.rb +5 -17
- data/bin/commands/rotate.rb +17 -0
- data/bin/commands/sections.rb +82 -7
- data/bin/commands/show.rb +8 -28
- data/bin/commands/since.rb +5 -16
- data/bin/commands/tag_dir.rb +27 -3
- data/bin/commands/today.rb +3 -28
- data/bin/commands/view.rb +3 -3
- data/bin/commands/yesterday.rb +3 -36
- data/bin/doing +29 -139
- data/docs/doc/Array.html +1 -1
- 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/Color.html +1 -1
- data/docs/doc/Doing/Completion.html +324 -4
- data/docs/doc/Doing/Configuration.html +3 -3
- 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/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/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +144 -3
- data/docs/doc/Doing/Items.html +209 -1
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Logger.html +1807 -0
- data/docs/doc/Doing/Note.html +109 -3
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +1 -1
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +1 -1
- data/docs/doc/Doing/Types.html +3 -3
- data/docs/doc/Doing/Util/Backup.html +1 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +8 -58
- data/docs/doc/Doing.html +4 -4
- 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/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 +1 -1
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +12 -10
- 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 +424 -304
- data/docs/doc/top-level-namespace.html +105 -1
- data/docs/index.md +1 -1
- data/doing.gemspec +24 -24
- data/doing.rdoc +259 -26
- data/example_plugin.rb +7 -5
- data/inputrc +57 -0
- data/lib/completion/_doing.zsh +48 -52
- data/lib/completion/doing.bash +14 -25
- data/lib/completion/doing.fish +41 -15
- data/lib/doing/add_options.rb +152 -0
- data/lib/doing/array/array.rb +16 -0
- data/lib/doing/changelog/changes.rb +1 -1
- data/lib/doing/chronify/string.rb +1 -1
- data/lib/doing/completion/bash_completion.rb +12 -51
- data/lib/doing/completion/fish_completion.rb +17 -53
- data/lib/doing/completion/zsh_completion.rb +21 -59
- data/lib/doing/completion.rb +203 -17
- data/lib/doing/configuration.rb +7 -1
- data/lib/doing/item.rb +30 -5
- data/lib/doing/items.rb +53 -5
- data/lib/doing/{log_adapter.rb → logger.rb} +8 -2
- data/lib/doing/note.rb +24 -8
- data/lib/doing/plugins/export/dayone_export.rb +8 -6
- data/lib/doing/plugins/export/html_export.rb +4 -4
- data/lib/doing/plugins/export/json_export.rb +19 -20
- data/lib/doing/plugins/export/markdown_export.rb +2 -2
- data/lib/doing/plugins/export/template_export.rb +4 -4
- data/lib/doing/plugins/import/calendar_import.rb +2 -2
- data/lib/doing/plugins/import/doing_import.rb +2 -2
- data/lib/doing/plugins/import/timing_import.rb +2 -2
- data/lib/doing/string/highlight.rb +3 -4
- data/lib/doing/string/string.rb +8 -0
- data/lib/doing/string/tags.rb +1 -1
- data/lib/doing/types.rb +2 -2
- data/lib/doing/util.rb +1 -1
- data/lib/doing/util_backup.rb +12 -12
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +119 -120
- data/lib/doing.rb +61 -3
- data/lib/examples/commands/wiki.rb +27 -19
- data/lib/examples/plugins/capture_thing_import.rb +1 -1
- data/lib/helpers/threaded_tests.rb +2 -0
- data/scripts/setting_replace.rb +11 -0
- metadata +109 -124
- data/.yardoc/checksums +0 -29
- data/.yardoc/complete +0 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/bin/commands/add_section.rb +0 -15
@@ -1,7 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Doing
|
2
4
|
module Completion
|
3
|
-
class
|
5
|
+
class ::String
|
6
|
+
def sanitize
|
7
|
+
gsub(/'/, '\\\'').gsub(/\[/, '(').gsub(/\]/, ')')
|
8
|
+
end
|
9
|
+
end
|
4
10
|
|
11
|
+
# Generate completions for zsh
|
12
|
+
class ZshCompletions
|
5
13
|
attr_accessor :commands, :global_options
|
6
14
|
|
7
15
|
def generate_helpers
|
@@ -34,58 +42,11 @@ module Doing
|
|
34
42
|
}
|
35
43
|
|
36
44
|
EOFUNCTIONS
|
45
|
+
@bar.advance(status: '✅')
|
37
46
|
@bar.finish
|
38
47
|
out
|
39
48
|
end
|
40
49
|
|
41
|
-
def get_help_sections(command = '')
|
42
|
-
res = `doing help #{command}`.strip
|
43
|
-
scanned = res.scan(/(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/)
|
44
|
-
sections = {}
|
45
|
-
scanned.each do |sect|
|
46
|
-
title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
|
47
|
-
content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
|
48
|
-
sections[title] = content
|
49
|
-
end
|
50
|
-
sections
|
51
|
-
end
|
52
|
-
|
53
|
-
def parse_option(option)
|
54
|
-
res = option.match(/(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>\w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/)
|
55
|
-
return nil unless res
|
56
|
-
|
57
|
-
{
|
58
|
-
short: res['short'],
|
59
|
-
long: res['long'],
|
60
|
-
arg: res[:arg],
|
61
|
-
description: res['desc'].short_desc
|
62
|
-
}
|
63
|
-
end
|
64
|
-
|
65
|
-
def parse_options(options)
|
66
|
-
options.map { |opt| parse_option(opt) }
|
67
|
-
end
|
68
|
-
|
69
|
-
def parse_command(command)
|
70
|
-
res = command.match(/^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/)
|
71
|
-
if res.nil?
|
72
|
-
Doing.logger.error('Completion:', "Error parsing #{command}")
|
73
|
-
return nil
|
74
|
-
|
75
|
-
end
|
76
|
-
commands = [res['cmd']]
|
77
|
-
commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
|
78
|
-
|
79
|
-
{
|
80
|
-
commands: commands,
|
81
|
-
description: res['desc'].short_desc
|
82
|
-
}
|
83
|
-
end
|
84
|
-
|
85
|
-
def parse_commands(commands)
|
86
|
-
commands.map { |cmd| parse_command(cmd) }
|
87
|
-
end
|
88
|
-
|
89
50
|
def generate_subcommand_completions
|
90
51
|
out = []
|
91
52
|
@commands.each_with_index do |cmd, i|
|
@@ -103,19 +64,19 @@ module Doing
|
|
103
64
|
@commands.each_with_index do |cmd, i|
|
104
65
|
@bar.advance(status: cmd[:commands].first)
|
105
66
|
|
106
|
-
data = get_help_sections(cmd[:commands].first)
|
67
|
+
data = Completion.get_help_sections(cmd[:commands].first)
|
107
68
|
option_arr = []
|
108
69
|
|
109
70
|
if data[:command_options]
|
110
|
-
parse_options(data[:command_options]).each do |option|
|
71
|
+
Completion.parse_options(data[:command_options]).each do |option|
|
111
72
|
next if option.nil?
|
112
73
|
|
113
|
-
arg = option[:arg] ?
|
74
|
+
arg = option[:arg] ? ":#{option[:arg]}:" : ''
|
114
75
|
|
115
76
|
option_arr << if option[:short]
|
116
|
-
%({-#{option[:short]}
|
77
|
+
%({'(--#{option[:long]})-#{option[:short]}','(-#{option[:short]})--#{option[:long]}'}"[#{option[:description].sanitize}]#{arg}")
|
117
78
|
else
|
118
|
-
%("
|
79
|
+
%("--#{option[:long]}[#{option[:description].sanitize}]#{arg}")
|
119
80
|
end
|
120
81
|
end
|
121
82
|
end
|
@@ -129,11 +90,12 @@ module Doing
|
|
129
90
|
end
|
130
91
|
|
131
92
|
def initialize
|
132
|
-
data = get_help_sections
|
133
|
-
@global_options = parse_options(data[:global_options])
|
134
|
-
@commands = parse_commands(data[:commands])
|
135
|
-
@bar = TTY::ProgressBar.new(" \033[0;0;33mGenerating Zsh completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count, bar_format: :
|
136
|
-
|
93
|
+
data = Completion.get_help_sections
|
94
|
+
@global_options = Completion.parse_options(data[:global_options])
|
95
|
+
@commands = Completion.parse_commands(data[:commands])
|
96
|
+
@bar = TTY::ProgressBar.new(" \033[0;0;33mGenerating Zsh completions: \033[0;35;40m[:bar] :status\033[0m", total: @commands.count + 1, bar_format: :square, hide_cursor: true, status: 'processing subcommands')
|
97
|
+
width = TTY::Screen.columns - 45
|
98
|
+
@bar.resize(width)
|
137
99
|
end
|
138
100
|
|
139
101
|
def generate_completions
|
data/lib/doing/completion.rb
CHANGED
@@ -11,25 +11,118 @@ require 'bash_completion'
|
|
11
11
|
module Doing
|
12
12
|
# Completion script generator
|
13
13
|
module Completion
|
14
|
+
OPTIONS_RX = /(?:-(?<short>\w), )?(?:--(?:\[no-\])?(?<long>\w+)(?:=(?<arg>\w+))?)\s+- (?<desc>.*?)$/.freeze
|
15
|
+
SECTIONS_RX = /(?m-i)^([A-Z ]+)\n([\s\S]*?)(?=\n+[A-Z]+|\Z)/.freeze
|
16
|
+
COMMAND_RX = /^(?<cmd>[^, \t]+)(?<alias>(?:, [^, \t]+)*)?\s+- (?<desc>.*?)$/.freeze
|
17
|
+
|
14
18
|
class << self
|
19
|
+
def get_help_sections(command = '')
|
20
|
+
res = `doing help #{command}`.strip
|
21
|
+
scanned = res.scan(SECTIONS_RX)
|
22
|
+
sections = {}
|
23
|
+
scanned.each do |sect|
|
24
|
+
title = sect[0].downcase.strip.gsub(/ +/, '_').to_sym
|
25
|
+
content = sect[1].split(/\n/).map(&:strip).delete_if(&:empty?)
|
26
|
+
sections[title] = content
|
27
|
+
end
|
28
|
+
sections
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse_option(option)
|
32
|
+
res = option.match(OPTIONS_RX)
|
33
|
+
return nil unless res
|
34
|
+
|
35
|
+
{
|
36
|
+
short: res['short'],
|
37
|
+
long: res['long'],
|
38
|
+
arg: res['arg'],
|
39
|
+
description: res['desc'].short_desc
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse_options(options)
|
44
|
+
options.map { |opt| parse_option(opt) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_command(command)
|
48
|
+
res = command.match(COMMAND_RX)
|
49
|
+
commands = [res['cmd']]
|
50
|
+
commands.concat(res['alias'].split(/, /).delete_if(&:empty?)) if res['alias']
|
51
|
+
|
52
|
+
{
|
53
|
+
commands: commands,
|
54
|
+
description: res['desc'].short_desc
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_commands(commands)
|
59
|
+
commands.map { |cmd| parse_command(cmd) }
|
60
|
+
end
|
61
|
+
|
15
62
|
# Generate a completion script and output to file or
|
16
63
|
# stdout
|
17
64
|
#
|
18
65
|
# @param type [String] shell to generate for (zsh|bash|fish)
|
19
66
|
# @param file [String] Path to save to, or 'stdout'
|
20
67
|
#
|
21
|
-
def generate_completion(type: 'zsh', file:
|
22
|
-
if type =~ /^all$/i
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
68
|
+
def generate_completion(type: 'zsh', file: :default, link: true)
|
69
|
+
return generate_all if type =~ /^all$/i
|
70
|
+
|
71
|
+
file = file == :default ? default_file(type) : file
|
72
|
+
file = validate_target(file)
|
73
|
+
result = generate_type(type)
|
74
|
+
|
75
|
+
if file =~ /^stdout$/i
|
76
|
+
$stdout.puts result
|
77
|
+
else
|
78
|
+
File.open(file, 'w') { |f| f.puts result }
|
79
|
+
Doing.logger.warn('File written:', "#{type} completions written to #{file}")
|
80
|
+
|
81
|
+
link_completion_type(type, file) if link
|
31
82
|
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def link_default(type)
|
86
|
+
type = normalize_type(type)
|
87
|
+
raise InvalidArgument, 'Unrecognized shell specified' if type == :invalid
|
88
|
+
|
89
|
+
return %i[zsh bash fish].each { |t| link_default(t) } if type == :all
|
32
90
|
|
91
|
+
install_builtin(type)
|
92
|
+
|
93
|
+
link_completion_type(type, File.join(default_dir, default_filenames[type]))
|
94
|
+
end
|
95
|
+
|
96
|
+
def install_builtin(type)
|
97
|
+
FileUtils.mkdir_p(default_dir)
|
98
|
+
src = File.expand_path(File.join(File.dirname(__FILE__), '..', 'completion', default_filenames[type]))
|
99
|
+
|
100
|
+
if File.exist?(File.join(default_dir, default_filenames[type]))
|
101
|
+
return unless Doing::Prompt.yn("Update #{type} completion script", default_response: 'n')
|
102
|
+
end
|
103
|
+
|
104
|
+
FileUtils.cp(src, default_dir)
|
105
|
+
Doing.logger.warn('File written:', "#{type} completions saved to #{default_file(type)}")
|
106
|
+
end
|
107
|
+
|
108
|
+
def normalize_type(type)
|
109
|
+
case type.to_s
|
110
|
+
when /^f/i
|
111
|
+
:fish
|
112
|
+
when /^b/i
|
113
|
+
:bash
|
114
|
+
when /^z/i
|
115
|
+
:zsh
|
116
|
+
when /^a/i
|
117
|
+
:all
|
118
|
+
else
|
119
|
+
:invalid
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def generate_type(type)
|
33
126
|
generator = case type.to_s
|
34
127
|
when /^f/i
|
35
128
|
FishCompletions.new
|
@@ -39,16 +132,109 @@ module Doing
|
|
39
132
|
ZshCompletions.new
|
40
133
|
end
|
41
134
|
|
42
|
-
|
135
|
+
generator.generate_completions
|
136
|
+
end
|
43
137
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
138
|
+
def validate_target(file)
|
139
|
+
unless file =~ /stdout/i
|
140
|
+
file = validate_file(file)
|
141
|
+
|
142
|
+
validate_dir(file)
|
143
|
+
end
|
144
|
+
|
145
|
+
file
|
146
|
+
end
|
147
|
+
|
148
|
+
def default_dir
|
149
|
+
File.expand_path('~/.local/share/doing/completion')
|
150
|
+
end
|
151
|
+
|
152
|
+
def default_filenames
|
153
|
+
{ zsh: '_doing.zsh', bash: 'doing.bash', fish: 'doing.fish' }
|
154
|
+
end
|
155
|
+
|
156
|
+
def default_file(type)
|
157
|
+
type = normalize_type(type)
|
158
|
+
|
159
|
+
File.join(default_dir, default_filenames[type])
|
160
|
+
end
|
161
|
+
|
162
|
+
def validate_file(file)
|
163
|
+
file = File.expand_path(file)
|
164
|
+
if File.exist?(file)
|
165
|
+
res = Doing::Prompt.yn("Overwrite #{file}", default_response: 'y')
|
166
|
+
raise UserCancelled unless res
|
167
|
+
|
168
|
+
FileUtils.rm(file) if res
|
169
|
+
end
|
170
|
+
file
|
171
|
+
end
|
172
|
+
|
173
|
+
def validate_dir(file)
|
174
|
+
dir = File.dirname(file)
|
175
|
+
unless File.directory?(dir)
|
176
|
+
res = Doing::Prompt.yn("#{dir} doesn't exist, create it", default_response: 'y')
|
177
|
+
raise UserCancelled unless res
|
178
|
+
|
179
|
+
FileUtils.mkdir_p(dir)
|
180
|
+
end
|
181
|
+
dir
|
182
|
+
end
|
183
|
+
|
184
|
+
def generate_all
|
185
|
+
Doing.logger.log_now(:warn, 'Generating:', 'all completion types, will use default paths')
|
186
|
+
generate_completion(type: 'fish', file: 'lib/completion/doing.fish', link: false)
|
187
|
+
Doing.logger.warn('File written:', 'fish completions written to lib/completion/doing.fish')
|
188
|
+
generate_completion(type: 'zsh', file: 'lib/completion/_doing.zsh', link: false)
|
189
|
+
Doing.logger.warn('File written:', 'zsh completions written to lib/completion/_doing.zsh')
|
190
|
+
generate_completion(type: 'bash', file: 'lib/completion/doing.bash', link: false)
|
191
|
+
Doing.logger.warn('File written:', 'bash completions written to lib/completion/doing.bash')
|
192
|
+
end
|
193
|
+
|
194
|
+
def link_completion_type(type, file)
|
195
|
+
dir = File.dirname(file)
|
196
|
+
case type.to_s
|
197
|
+
when /^b/i
|
198
|
+
unless dir =~ %r{(\.bash_it/completion|bash_completion/completions)}
|
199
|
+
link_completion(file, ['~/.bash_it/completion/enabled', '/usr/share/bash_completion/completions'], 'doing.bash')
|
49
200
|
end
|
50
|
-
|
201
|
+
when /^f/i
|
202
|
+
link_completion(file, ['~/.config/fish/completions'], 'doing.fish') unless dir =~ %r{.config/fish/completions}
|
203
|
+
when /^z/i
|
204
|
+
unless dir =~ %r{(\.oh-my-zsh/completions|share/site-functions)}
|
205
|
+
link_completion(file, ['~/.oh-my-zsh/completions', '/usr/local/share/zsh/site-functions'], '_doing.zsh')
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def link_completion(file, targets, filename)
|
211
|
+
return if targets.map { |t| File.expand_path(t) }.include?(File.dirname(file))
|
212
|
+
|
213
|
+
found = false
|
214
|
+
linked = false
|
215
|
+
|
216
|
+
targets.each do |target|
|
217
|
+
next unless File.directory?(File.expand_path(target))
|
218
|
+
found = true
|
219
|
+
|
220
|
+
target_file = File.join(File.expand_path(target), filename)
|
221
|
+
next unless Doing::Prompt.yn("Create link to #{target_file}", default_response: 'n')
|
222
|
+
|
223
|
+
FileUtils.ln_s(File.expand_path(file), target_file, force: true)
|
224
|
+
Doing.logger.warn('File linked:', "#{File.expand_path(file)} -> #{target_file}")
|
225
|
+
linked = true
|
226
|
+
break
|
227
|
+
end
|
228
|
+
|
229
|
+
return if linked
|
230
|
+
|
231
|
+
unless found
|
232
|
+
$stdout.puts 'No known auto-load directory found for specified shell'.red
|
233
|
+
$stdout.puts "Looked for #{targets.join(', ')}, found no existing directory".yellow
|
51
234
|
end
|
235
|
+
$stdout.puts 'If you don\'t want to autoload completions'.yellow
|
236
|
+
$stdout.puts 'you can source the script directly in your shell\'s startup file:'.yellow
|
237
|
+
$stdout.puts %(source "#{file}").boldwhite
|
52
238
|
end
|
53
239
|
end
|
54
240
|
end
|
data/lib/doing/configuration.rb
CHANGED
@@ -156,7 +156,13 @@ module Doing
|
|
156
156
|
##
|
157
157
|
## @return [String] file path
|
158
158
|
##
|
159
|
-
def choose_config(create: false)
|
159
|
+
def choose_config(create: false, local: false)
|
160
|
+
if local && create
|
161
|
+
res = File.expand_path('.doingrc')
|
162
|
+
FileUtils.touch(res)
|
163
|
+
return res
|
164
|
+
end
|
165
|
+
|
160
166
|
return @config_file if @force_answer
|
161
167
|
|
162
168
|
if @additional_configs&.count&.positive? || create
|
data/lib/doing/item.rb
CHANGED
@@ -35,6 +35,8 @@ module Doing
|
|
35
35
|
|
36
36
|
## If the entry doesn't have a @done date, return the elapsed time
|
37
37
|
def duration
|
38
|
+
return nil unless should_time? && should_finish?
|
39
|
+
|
38
40
|
return nil if @title =~ /(?<=^| )@done\b/
|
39
41
|
|
40
42
|
return Time.now - @date
|
@@ -88,17 +90,20 @@ module Doing
|
|
88
90
|
##
|
89
91
|
## Test for equality between items
|
90
92
|
##
|
91
|
-
## @param other
|
93
|
+
## @param other [Item] The other item
|
94
|
+
## @param match_section [Boolean] If true, require item sections to match
|
92
95
|
##
|
93
96
|
## @return [Boolean] is equal?
|
94
97
|
##
|
95
|
-
def equal?(other)
|
98
|
+
def equal?(other, match_section: false)
|
96
99
|
return false if @title.strip != other.title.strip
|
97
100
|
|
98
101
|
return false if @date != other.date
|
99
102
|
|
100
103
|
return false unless @note.equal?(other.note)
|
101
104
|
|
105
|
+
return false if match_section && @section != other.section
|
106
|
+
|
102
107
|
true
|
103
108
|
end
|
104
109
|
|
@@ -272,7 +277,7 @@ module Doing
|
|
272
277
|
end
|
273
278
|
|
274
279
|
def highlight_search(search, distance: nil, negate: false, case_type: nil)
|
275
|
-
prefs = Doing.
|
280
|
+
prefs = Doing.setting('search', {})
|
276
281
|
matching = prefs.fetch('matching', 'pattern').normalize_matching
|
277
282
|
distance ||= prefs.fetch('distance', 3).to_i
|
278
283
|
case_type ||= prefs.fetch('case', 'smart').normalize_case
|
@@ -311,7 +316,7 @@ module Doing
|
|
311
316
|
## @return [Boolean] matches search criteria
|
312
317
|
##
|
313
318
|
def search(search, distance: nil, negate: false, case_type: nil)
|
314
|
-
prefs = Doing.
|
319
|
+
prefs = Doing.setting('search', {})
|
315
320
|
matching = prefs.fetch('matching', 'pattern').normalize_matching
|
316
321
|
distance ||= prefs.fetch('distance', 3).to_i
|
317
322
|
case_type ||= prefs.fetch('case', 'smart').normalize_case
|
@@ -354,6 +359,24 @@ module Doing
|
|
354
359
|
negate ? !matches : matches
|
355
360
|
end
|
356
361
|
|
362
|
+
##
|
363
|
+
## Test if item has a @done tag
|
364
|
+
##
|
365
|
+
## @return [Boolean] true item has @done tag
|
366
|
+
##
|
367
|
+
def finished?
|
368
|
+
tags?('done')
|
369
|
+
end
|
370
|
+
|
371
|
+
##
|
372
|
+
## Test if item does not contain @done tag
|
373
|
+
##
|
374
|
+
## @return [Boolean] true if item is missing @done tag
|
375
|
+
##
|
376
|
+
def unfinished?
|
377
|
+
tags?('done', negate: true)
|
378
|
+
end
|
379
|
+
|
357
380
|
##
|
358
381
|
## Test if item is included in never_finish config and
|
359
382
|
## thus should not receive a @done tag
|
@@ -436,7 +459,7 @@ module Doing
|
|
436
459
|
private
|
437
460
|
|
438
461
|
def should?(key)
|
439
|
-
config = Doing.
|
462
|
+
config = Doing.settings
|
440
463
|
return true unless config[key].is_a?(Array)
|
441
464
|
|
442
465
|
config[key].each do |tag|
|
@@ -451,6 +474,8 @@ module Doing
|
|
451
474
|
end
|
452
475
|
|
453
476
|
def calc_interval
|
477
|
+
return nil unless should_time? && should_finish?
|
478
|
+
|
454
479
|
done = end_date
|
455
480
|
return nil if done.nil?
|
456
481
|
|
data/lib/doing/items.rb
CHANGED
@@ -58,6 +58,24 @@ module Doing
|
|
58
58
|
Doing.logger.info('New section:', %("#{section}" added)) if log
|
59
59
|
end
|
60
60
|
|
61
|
+
def delete_section(section, log: false)
|
62
|
+
return unless section?(section)
|
63
|
+
|
64
|
+
raise DoingRuntimeError, 'Section not empty' if in_section(section).count > 0
|
65
|
+
|
66
|
+
deleted = false
|
67
|
+
|
68
|
+
@sections.each do |sect|
|
69
|
+
if sect.title == section && in_section(sect).count.zero?
|
70
|
+
@sections.delete(sect)
|
71
|
+
Doing.logger.info('Removed section:', %("#{section}" removed)) if log
|
72
|
+
return
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
Doing.logger.error('Not found:', %("#{section}" not found))
|
77
|
+
end
|
78
|
+
|
61
79
|
# Get a new Items object containing only items in a
|
62
80
|
# specified section
|
63
81
|
#
|
@@ -126,14 +144,44 @@ module Doing
|
|
126
144
|
diff
|
127
145
|
end
|
128
146
|
|
147
|
+
##
|
148
|
+
## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
|
149
|
+
##
|
150
|
+
## @return [Items] Items array with duplicate entries removed
|
151
|
+
##
|
152
|
+
def dedup(match_section: true)
|
153
|
+
unique = Items.new
|
154
|
+
each do |item|
|
155
|
+
unique.push(item) unless unique.include?(item, match_section: match_section)
|
156
|
+
end
|
157
|
+
|
158
|
+
unique
|
159
|
+
end
|
160
|
+
|
161
|
+
def dedup!(match_section: true)
|
162
|
+
replace dedup(match_section: match_section)
|
163
|
+
end
|
164
|
+
|
165
|
+
def include?(item, match_section: true)
|
166
|
+
includes = false
|
167
|
+
each do |other_item|
|
168
|
+
if other_item.equal?(item, match_section: match_section)
|
169
|
+
includes = true
|
170
|
+
break
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
includes
|
175
|
+
end
|
176
|
+
|
129
177
|
# Output sections and items in Doing file format
|
130
178
|
def to_s
|
131
179
|
out = []
|
132
180
|
@sections.each do |section|
|
133
181
|
out.push(section.original)
|
134
|
-
items = in_section(section.title).sort_by
|
135
|
-
items.reverse! if Doing.
|
136
|
-
items.each { |item| out.push(item.to_s)}
|
182
|
+
items = in_section(section.title).sort_by(&:date)
|
183
|
+
items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
|
184
|
+
items.each { |item| out.push(item.to_s) }
|
137
185
|
end
|
138
186
|
|
139
187
|
out.join("\n")
|
@@ -141,8 +189,8 @@ module Doing
|
|
141
189
|
|
142
190
|
# @private
|
143
191
|
def inspect
|
144
|
-
|
192
|
+
sections = @sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')
|
193
|
+
"#<Doing::Items #{count} items, #{@sections.count} sections: #{sections}>"
|
145
194
|
end
|
146
|
-
|
147
195
|
end
|
148
196
|
end
|
@@ -4,7 +4,7 @@ module Doing
|
|
4
4
|
##
|
5
5
|
## Log adapter
|
6
6
|
##
|
7
|
-
class
|
7
|
+
class Logger
|
8
8
|
# Sets the log device
|
9
9
|
attr_writer :logdev
|
10
10
|
|
@@ -332,9 +332,15 @@ module Doing
|
|
332
332
|
if tags_added.empty?
|
333
333
|
count(:skipped, level: :debug, message: 'no tags added to %count %items')
|
334
334
|
elsif single && item
|
335
|
+
elapsed = if item && tags_added.include?('done')
|
336
|
+
item.interval ? " (#{item.interval&.time_string(format: :dhm)})" : ''
|
337
|
+
else
|
338
|
+
''
|
339
|
+
end
|
340
|
+
|
335
341
|
added = tags_added.log_tags
|
336
342
|
info('Tagged:',
|
337
|
-
%(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{added} to #{item.title}))
|
343
|
+
%(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{added}#{elapsed} to #{item.title}))
|
338
344
|
else
|
339
345
|
count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
|
340
346
|
end
|
data/lib/doing/note.rb
CHANGED
@@ -5,7 +5,6 @@ module Doing
|
|
5
5
|
## This class describes an item note.
|
6
6
|
##
|
7
7
|
class Note < Array
|
8
|
-
|
9
8
|
##
|
10
9
|
## Initializes a new note
|
11
10
|
##
|
@@ -28,9 +27,10 @@ module Doing
|
|
28
27
|
##
|
29
28
|
def add(note, replace: false)
|
30
29
|
clear if replace
|
31
|
-
|
30
|
+
case note
|
31
|
+
when String
|
32
32
|
append_string(note)
|
33
|
-
|
33
|
+
when Array
|
34
34
|
append(note)
|
35
35
|
end
|
36
36
|
end
|
@@ -55,7 +55,7 @@ module Doing
|
|
55
55
|
## @return [Array] Stripped note
|
56
56
|
##
|
57
57
|
def strip_lines
|
58
|
-
map(&:strip)
|
58
|
+
Note.new(map(&:strip))
|
59
59
|
end
|
60
60
|
|
61
61
|
def strip_lines!
|
@@ -64,8 +64,24 @@ module Doing
|
|
64
64
|
|
65
65
|
##
|
66
66
|
## Note as multi-line string
|
67
|
-
|
68
|
-
|
67
|
+
##
|
68
|
+
## @param prefix [String] prefix for each line (default two tabs, TaskPaper format)
|
69
|
+
##
|
70
|
+
def to_s(prefix: "\t\t")
|
71
|
+
compress.strip_lines.map { |l| "#{prefix}#{l}" }.join("\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
## Returns note as a single line, newlines separated by
|
76
|
+
## space
|
77
|
+
##
|
78
|
+
## @return [String] Line representation of the Note.
|
79
|
+
##
|
80
|
+
## @param separator The separator with which to
|
81
|
+
## join multiple lines
|
82
|
+
##
|
83
|
+
def to_line(separator: ' ')
|
84
|
+
compress.strip_lines.join(separator)
|
69
85
|
end
|
70
86
|
|
71
87
|
# @private
|
@@ -94,7 +110,7 @@ module Doing
|
|
94
110
|
## @param lines [Array] Array of strings
|
95
111
|
##
|
96
112
|
def append(lines)
|
97
|
-
concat(lines)
|
113
|
+
concat(lines.utf8)
|
98
114
|
replace compress
|
99
115
|
end
|
100
116
|
|
@@ -105,7 +121,7 @@ module Doing
|
|
105
121
|
## newlines will be split
|
106
122
|
##
|
107
123
|
def append_string(input)
|
108
|
-
concat(input.split(/\n/).map(&:strip))
|
124
|
+
concat(input.utf8.split(/\n/).map(&:strip))
|
109
125
|
replace compress
|
110
126
|
end
|
111
127
|
end
|