doing 2.1.30 → 2.1.34
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/.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
|