doing 2.0.25 → 2.1.0pre
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/.yardoc/checksums +18 -15
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile.lock +8 -1
- data/README.md +1 -1
- data/Rakefile +23 -4
- data/bin/doing +205 -127
- data/doc/Array.html +354 -1
- data/doc/Doing/Color.html +104 -92
- data/doc/Doing/Completion.html +216 -0
- data/doc/Doing/Configuration.html +340 -5
- data/doc/Doing/Content.html +229 -0
- data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/doc/Doing/Errors/EmptyInput.html +1 -1
- data/doc/Doing/Errors/NoResults.html +1 -1
- data/doc/Doing/Errors/PluginException.html +1 -1
- data/doc/Doing/Errors/UserCancelled.html +1 -1
- data/doc/Doing/Errors/WrongCommand.html +1 -1
- data/doc/Doing/Errors.html +1 -1
- data/doc/Doing/Hooks.html +1 -1
- data/doc/Doing/Item.html +337 -49
- data/doc/Doing/Items.html +444 -35
- data/doc/Doing/LogAdapter.html +139 -51
- data/doc/Doing/Note.html +253 -22
- data/doc/Doing/Pager.html +74 -36
- data/doc/Doing/Plugins.html +1 -1
- data/doc/Doing/Prompt.html +674 -0
- data/doc/Doing/Section.html +354 -0
- data/doc/Doing/Util.html +57 -1
- data/doc/Doing/WWID.html +477 -670
- data/doc/Doing/WWIDFile.html +398 -0
- data/doc/Doing.html +5 -5
- data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/doc/GLI/Commands.html +1 -1
- data/doc/GLI.html +1 -1
- data/doc/Hash.html +97 -1
- data/doc/Status.html +37 -3
- data/doc/String.html +599 -23
- data/doc/Symbol.html +3 -3
- data/doc/Time.html +1 -1
- data/doc/_index.html +22 -1
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +8 -2
- data/doc/index.html +8 -2
- data/doc/method_list.html +453 -173
- data/doc/top-level-namespace.html +1 -1
- data/doing.gemspec +3 -0
- data/doing.rdoc +40 -12
- data/example_plugin.rb +3 -3
- data/lib/completion/_doing.zsh +1 -1
- data/lib/completion/doing.bash +8 -8
- data/lib/completion/doing.fish +1 -1
- data/lib/doing/array.rb +36 -0
- data/lib/doing/colors.rb +70 -66
- data/lib/doing/completion.rb +6 -0
- data/lib/doing/configuration.rb +69 -28
- data/lib/doing/hash.rb +37 -0
- data/lib/doing/item.rb +77 -12
- data/lib/doing/items.rb +125 -0
- data/lib/doing/log_adapter.rb +55 -3
- data/lib/doing/note.rb +53 -1
- data/lib/doing/pager.rb +49 -38
- data/lib/doing/plugins/export/markdown_export.rb +4 -4
- data/lib/doing/plugins/export/template_export.rb +2 -2
- data/lib/doing/plugins/import/calendar_import.rb +4 -4
- data/lib/doing/plugins/import/doing_import.rb +5 -7
- data/lib/doing/plugins/import/timing_import.rb +3 -3
- data/lib/doing/prompt.rb +206 -0
- data/lib/doing/section.rb +30 -0
- data/lib/doing/string.rb +103 -27
- data/lib/doing/util.rb +14 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +306 -621
- data/lib/doing.rb +6 -2
- data/lib/examples/plugins/capture_thing_import.rb +162 -0
- metadata +73 -5
- data/lib/doing/wwidfile.rb +0 -117
@@ -34,9 +34,7 @@ module Doing
|
|
34
34
|
tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
|
35
35
|
prefix = options[:prefix] || ''
|
36
36
|
|
37
|
-
@old_items =
|
38
|
-
|
39
|
-
wwid.content.each { |_, v| @old_items.concat(v[:items]) }
|
37
|
+
@old_items = wwid.content.dup
|
40
38
|
|
41
39
|
new_items = read_doing_file(path)
|
42
40
|
|
@@ -46,7 +44,7 @@ module Doing
|
|
46
44
|
new_items = wwid.filter_items(new_items, opt: options)
|
47
45
|
|
48
46
|
skipped = total - new_items.count
|
49
|
-
Doing.logger.debug('Skipped:'
|
47
|
+
Doing.logger.debug('Skipped:', %(#{skipped} items that didn't match filter criteria)) if skipped.positive?
|
50
48
|
|
51
49
|
imported = []
|
52
50
|
|
@@ -76,13 +74,13 @@ module Doing
|
|
76
74
|
dups = new_items.count - imported.count
|
77
75
|
Doing.logger.info('Skipped:', %(#{dups} duplicate items)) if dups.positive?
|
78
76
|
|
79
|
-
imported = wwid.dedup(imported, !options[:overlap])
|
77
|
+
imported = wwid.dedup(imported, no_overlap: !options[:overlap])
|
80
78
|
overlaps = new_items.count - imported.count - dups
|
81
79
|
Doing.logger.debug('Skipped:', "#{overlaps} items with overlapping times") if overlaps.positive?
|
82
80
|
|
83
81
|
imported.each do |item|
|
84
|
-
wwid.add_section(item.section) unless wwid.content.
|
85
|
-
wwid.content
|
82
|
+
wwid.content.add_section(item.section) unless wwid.content.section?(item.section)
|
83
|
+
wwid.content.push(item)
|
86
84
|
end
|
87
85
|
|
88
86
|
Doing.logger.info('Imported:', "#{imported.count} items")
|
@@ -27,7 +27,7 @@ module Doing
|
|
27
27
|
section = options[:section] || wwid.config['current_section']
|
28
28
|
options[:no_overlap] ||= false
|
29
29
|
options[:autotag] ||= wwid.auto_tag
|
30
|
-
wwid.add_section(section) unless wwid.content.
|
30
|
+
wwid.content.add_section(section) unless wwid.content.section?(section)
|
31
31
|
|
32
32
|
add_tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
|
33
33
|
prefix = options[:prefix] || '[Timing.app]'
|
@@ -73,11 +73,11 @@ module Doing
|
|
73
73
|
filtered = skipped - new_items.count
|
74
74
|
Doing.logger.debug('Skipped:' , %(#{filtered} items that didn't match filter criteria)) if filtered.positive?
|
75
75
|
|
76
|
-
new_items = wwid.dedup(new_items, options[:no_overlap])
|
76
|
+
new_items = wwid.dedup(new_items, no_overlap: options[:no_overlap])
|
77
77
|
dups = filtered - new_items.count
|
78
78
|
Doing.logger.debug('Skipped:' , %(#{dups} items with overlapping times)) if dups.positive?
|
79
79
|
|
80
|
-
wwid.content
|
80
|
+
wwid.content.concat(new_items)
|
81
81
|
Doing.logger.info('Imported:', %(#{new_items.count} items to #{section}))
|
82
82
|
end
|
83
83
|
|
data/lib/doing/prompt.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
# Terminal Prompt methods
|
5
|
+
module Prompt
|
6
|
+
class << self
|
7
|
+
attr_writer :force_answer, :default_answer
|
8
|
+
|
9
|
+
include Color
|
10
|
+
|
11
|
+
def force_answer
|
12
|
+
@force_answer ||= nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_answer
|
16
|
+
@default_answer ||= false
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
## Ask a yes or no question in the terminal
|
21
|
+
##
|
22
|
+
## @param question [String] The question
|
23
|
+
## to ask
|
24
|
+
## @param default_response (Bool) default
|
25
|
+
## response if no input
|
26
|
+
##
|
27
|
+
## @return (Bool) yes or no
|
28
|
+
##
|
29
|
+
def yn(question, default_response: false)
|
30
|
+
unless @force_answer.nil?
|
31
|
+
return @force_answer
|
32
|
+
end
|
33
|
+
|
34
|
+
default = if default_response.is_a?(String)
|
35
|
+
default_response =~ /y/i ? true : false
|
36
|
+
else
|
37
|
+
default_response
|
38
|
+
end
|
39
|
+
|
40
|
+
# if global --default is set, answer default
|
41
|
+
return default if @default_answer
|
42
|
+
|
43
|
+
# if this isn't an interactive shell, answer default
|
44
|
+
return default unless $stdout.isatty
|
45
|
+
|
46
|
+
# clear the buffer
|
47
|
+
if ARGV&.length
|
48
|
+
ARGV.length.times do
|
49
|
+
ARGV.shift
|
50
|
+
end
|
51
|
+
end
|
52
|
+
system 'stty cbreak'
|
53
|
+
|
54
|
+
cw = white
|
55
|
+
cbw = boldwhite
|
56
|
+
cbg = boldgreen
|
57
|
+
cd = Color.default
|
58
|
+
|
59
|
+
options = unless default.nil?
|
60
|
+
"#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
|
61
|
+
else
|
62
|
+
"#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
|
63
|
+
end
|
64
|
+
$stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
|
65
|
+
res = $stdin.sysread 1
|
66
|
+
puts
|
67
|
+
system 'stty cooked'
|
68
|
+
|
69
|
+
res.chomp!
|
70
|
+
res.downcase!
|
71
|
+
|
72
|
+
return default if res.empty?
|
73
|
+
|
74
|
+
res =~ /y/i ? true : false
|
75
|
+
end
|
76
|
+
|
77
|
+
def fzf
|
78
|
+
@fzf ||= install_fzf
|
79
|
+
end
|
80
|
+
|
81
|
+
def install_fzf
|
82
|
+
fzf_dir = File.join(File.dirname(__FILE__), '../helpers/fzf')
|
83
|
+
FileUtils.mkdir_p(fzf_dir) unless File.directory?(fzf_dir)
|
84
|
+
fzf_bin = File.join(fzf_dir, 'bin/fzf')
|
85
|
+
return fzf_bin if File.exist?(fzf_bin)
|
86
|
+
|
87
|
+
prev_level = Doing.logger.level
|
88
|
+
Doing.logger.adjust_verbosity({ log_level: :info })
|
89
|
+
Doing.logger.log_now(:warn, 'Compiling and installing fzf -- this will only happen once')
|
90
|
+
Doing.logger.log_now(:warn, 'fzf is copyright Junegunn Choi, MIT License <https://github.com/junegunn/fzf/blob/master/LICENSE>')
|
91
|
+
|
92
|
+
system("'#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
|
93
|
+
unless File.exist?(fzf_bin)
|
94
|
+
Doing.logger.log_now(:warn, 'Error installing, trying again as root')
|
95
|
+
system("sudo '#{fzf_dir}/install' --bin --no-key-bindings --no-completion --no-update-rc --no-bash --no-zsh --no-fish &> /dev/null")
|
96
|
+
end
|
97
|
+
raise RuntimeError.new('Error installing fzf, please report at https://github.com/ttscoff/doing/issues') unless File.exist?(fzf_bin)
|
98
|
+
|
99
|
+
Doing.logger.info("fzf installed to #{fzf}")
|
100
|
+
Doing.logger.adjust_verbosity({ log_level: prev_level })
|
101
|
+
fzf_bin
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
## Generate a menu of options and allow user selection
|
106
|
+
##
|
107
|
+
## @return [String] The selected option
|
108
|
+
##
|
109
|
+
def choose_from(options, prompt: 'Make a selection: ', multiple: false, sorted: true, fzf_args: [])
|
110
|
+
return nil unless $stdout.isatty
|
111
|
+
|
112
|
+
# fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
|
113
|
+
fzf_args << %(--prompt "#{prompt}")
|
114
|
+
fzf_args << '--multi' if multiple
|
115
|
+
header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
|
116
|
+
fzf_args << %(--header "#{header}")
|
117
|
+
options.sort! if sorted
|
118
|
+
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
|
119
|
+
return false if res.strip.size.zero?
|
120
|
+
|
121
|
+
res
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
## Create an interactive menu to select from a set of Items
|
126
|
+
##
|
127
|
+
## @param items [Array] list of items
|
128
|
+
## @param opt [Hash] options
|
129
|
+
## @param include_section [Boolean] include section
|
130
|
+
##
|
131
|
+
## @option opt [String] :header
|
132
|
+
## @option opt [String] :prompt
|
133
|
+
## @option opt [String] :query
|
134
|
+
## @option opt [Boolean] :show_if_single
|
135
|
+
## @option opt [Boolean] :menu
|
136
|
+
## @option opt [Boolean] :sort
|
137
|
+
## @option opt [Boolean] :multiple
|
138
|
+
## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
|
139
|
+
##
|
140
|
+
def choose_from_items(items, **opt)
|
141
|
+
return items unless $stdout.isatty
|
142
|
+
|
143
|
+
return nil unless items.count.positive?
|
144
|
+
|
145
|
+
case_sensitive = opt.fetch(:case, :smart).normalize_case
|
146
|
+
header = opt.fetch(:header, 'Arrows: navigate, tab: mark for selection, ctrl-a: select all, enter: commit')
|
147
|
+
prompt = opt.fetch(:prompt, 'Select entries to act on > ')
|
148
|
+
query = opt.fetch(:query) { opt.fetch(:search, '') }
|
149
|
+
include_section = opt.fetch(:include_section, false)
|
150
|
+
|
151
|
+
pad = items.length.to_s.length
|
152
|
+
options = items.map.with_index do |item, i|
|
153
|
+
out = [
|
154
|
+
format("%#{pad}d", i),
|
155
|
+
') ',
|
156
|
+
format('%13s', item.date.relative_date),
|
157
|
+
' | ',
|
158
|
+
item.title
|
159
|
+
]
|
160
|
+
if include_section
|
161
|
+
out.concat([
|
162
|
+
' (',
|
163
|
+
item.section,
|
164
|
+
') '
|
165
|
+
])
|
166
|
+
end
|
167
|
+
out.join('')
|
168
|
+
end
|
169
|
+
|
170
|
+
fzf_args = [
|
171
|
+
%(--header="#{header}"),
|
172
|
+
%(--prompt="#{prompt.sub(/ *$/, ' ')}"),
|
173
|
+
opt.fetch(:multiple) ? '--multi' : '--no-multi',
|
174
|
+
'-0',
|
175
|
+
'--bind ctrl-a:select-all',
|
176
|
+
%(-q "#{query}"),
|
177
|
+
'--info=inline'
|
178
|
+
]
|
179
|
+
fzf_args.push('-1') unless opt.fetch(:show_if_single)
|
180
|
+
fzf_args << case case_sensitive
|
181
|
+
when :sensitive
|
182
|
+
'+i'
|
183
|
+
when :ignore
|
184
|
+
'-i'
|
185
|
+
end
|
186
|
+
fzf_args << '-e' if opt.fetch(:exact, false)
|
187
|
+
|
188
|
+
|
189
|
+
unless opt.fetch(:menu)
|
190
|
+
raise InvalidArgument, "Can't skip menu when no query is provided" unless query && !query.empty?
|
191
|
+
|
192
|
+
fzf_args.concat([%(--filter="#{query}"), opt.fetch(:sort) ? '' : '--no-sort'])
|
193
|
+
end
|
194
|
+
|
195
|
+
res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
|
196
|
+
selected = []
|
197
|
+
res.split(/\n/).each do |item|
|
198
|
+
idx = item.match(/^ *(\d+)\)/)[1].to_i
|
199
|
+
selected.push(items[idx])
|
200
|
+
end
|
201
|
+
|
202
|
+
opt.fetch(:multiple) ? selected : selected[0]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
# Section Object
|
5
|
+
class Section
|
6
|
+
attr_accessor :original, :title
|
7
|
+
|
8
|
+
def initialize(title, original: nil)
|
9
|
+
super()
|
10
|
+
|
11
|
+
@title = title
|
12
|
+
|
13
|
+
@original = if original.nil?
|
14
|
+
"#{title}:"
|
15
|
+
else
|
16
|
+
original =~ /:(\s+@\S+(\(.*?\))?)*$/ ? original : "#{original}:"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Outputs section title
|
21
|
+
def to_s
|
22
|
+
@title
|
23
|
+
end
|
24
|
+
|
25
|
+
# @private
|
26
|
+
def inspect
|
27
|
+
%(#<Doing::Section @title="#{@title}" @original="#{@original}">)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/doing/string.rb
CHANGED
@@ -20,8 +20,7 @@ module Doing
|
|
20
20
|
## can be separated by up to *distance* characters in
|
21
21
|
## haystack, spaces indicate unlimited distance.
|
22
22
|
##
|
23
|
-
## @example "this word".to_rx(2) =>
|
24
|
-
## /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/
|
23
|
+
## @example `"this word".to_rx(2) => /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/`
|
25
24
|
##
|
26
25
|
## @param distance [Integer] Allowed distance
|
27
26
|
## between characters
|
@@ -63,6 +62,15 @@ module Doing
|
|
63
62
|
end
|
64
63
|
end
|
65
64
|
|
65
|
+
# Compress multiple spaces to single space
|
66
|
+
def compress
|
67
|
+
gsub(/ +/, ' ').strip
|
68
|
+
end
|
69
|
+
|
70
|
+
def compress!
|
71
|
+
replace compress
|
72
|
+
end
|
73
|
+
|
66
74
|
## @param (see #highlight_tags)
|
67
75
|
def highlight_tags!(color = 'yellow')
|
68
76
|
replace highlight_tags(color)
|
@@ -289,11 +297,29 @@ module Doing
|
|
289
297
|
title
|
290
298
|
end
|
291
299
|
|
292
|
-
|
293
|
-
|
300
|
+
##
|
301
|
+
## Add, rename, or remove a tag in place
|
302
|
+
##
|
303
|
+
## @see #tag
|
304
|
+
##
|
305
|
+
def tag!(tag, **options)
|
306
|
+
replace tag(tag, **options)
|
294
307
|
end
|
295
308
|
|
296
|
-
|
309
|
+
##
|
310
|
+
## Add, rename, or remove a tag
|
311
|
+
##
|
312
|
+
## @param tag The tag
|
313
|
+
## @param value [String] Value for tag (@tag(value))
|
314
|
+
## @param remove [Boolean] Remove the tag instead of adding
|
315
|
+
## @param rename_to [String] Replace tag with this tag
|
316
|
+
## @param regex [Boolean] Tag is regular expression
|
317
|
+
## @param single [Boolean] Operating on a single item (for logging)
|
318
|
+
## @param force [Boolean] With rename_to, add tag if it doesn't exist
|
319
|
+
##
|
320
|
+
## @return [String] The string with modified tags
|
321
|
+
##
|
322
|
+
def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: false)
|
297
323
|
log_level = single ? :info : :debug
|
298
324
|
title = dup
|
299
325
|
title.chomp!
|
@@ -307,13 +333,14 @@ module Doing
|
|
307
333
|
end
|
308
334
|
|
309
335
|
if remove || rename_to
|
310
|
-
|
336
|
+
rx = Regexp.new("(?<=^| )@#{rx_tag}(?<parens>\\((?<value>[^)]*)\\))?(?= |$)", case_sensitive)
|
337
|
+
m = title.match(rx)
|
311
338
|
|
312
|
-
|
313
|
-
|
339
|
+
if m.nil? && rename_to && force
|
340
|
+
title.tag!(rename_to, value: value, single: single)
|
341
|
+
elsif m
|
314
342
|
title.gsub!(rx) do
|
315
|
-
m
|
316
|
-
rename_to ? "#{m[1]}@#{rename_to}#{m[2]}" : m[1]
|
343
|
+
rename_to ? "@#{rename_to}#{value.nil? ? m['parens'] : "(#{value})"}" : ''
|
317
344
|
end
|
318
345
|
|
319
346
|
title.dedup_tags!
|
@@ -373,9 +400,16 @@ module Doing
|
|
373
400
|
title
|
374
401
|
end
|
375
402
|
|
376
|
-
# Returns the last escape sequence from a string
|
403
|
+
# Returns the last escape sequence from a string.
|
377
404
|
#
|
378
|
-
#
|
405
|
+
# Actually returns all escape codes, with the assumption
|
406
|
+
# that the result of inserting them will generate the
|
407
|
+
# same color as was set at the end of the string.
|
408
|
+
# Because you can send modifiers like dark and bold
|
409
|
+
# separate from color codes, only using the last code
|
410
|
+
# may not render the same style.
|
411
|
+
#
|
412
|
+
# @return [String] All escape codes in string
|
379
413
|
#
|
380
414
|
def last_color
|
381
415
|
scan(/\e\[[\d;]+m/).join('')
|
@@ -386,17 +420,20 @@ module Doing
|
|
386
420
|
##
|
387
421
|
## @param opt [Hash] Additional Options
|
388
422
|
##
|
389
|
-
def link_urls!(opt
|
390
|
-
|
423
|
+
def link_urls!(**opt)
|
424
|
+
fmt = opt.fetch(:format, :html)
|
425
|
+
replace link_urls(format: fmt)
|
391
426
|
end
|
392
427
|
|
393
|
-
def link_urls(opt
|
394
|
-
opt
|
428
|
+
def link_urls(**opt)
|
429
|
+
fmt = opt.fetch(:format, :html)
|
430
|
+
return self unless fmt
|
431
|
+
|
395
432
|
str = dup
|
396
433
|
|
397
|
-
str = str.remove_self_links if
|
434
|
+
str = str.remove_self_links if fmt == :markdown
|
398
435
|
|
399
|
-
str.replace_qualified_urls(format:
|
436
|
+
str.replace_qualified_urls(format: fmt).clean_unlinked_urls
|
400
437
|
end
|
401
438
|
|
402
439
|
# Remove <self-linked> formatting
|
@@ -412,21 +449,24 @@ module Doing
|
|
412
449
|
end
|
413
450
|
|
414
451
|
# Replace qualified urls
|
415
|
-
def replace_qualified_urls(
|
416
|
-
|
452
|
+
def replace_qualified_urls(**options)
|
453
|
+
fmt = options.fetch(:format, :html)
|
417
454
|
gsub(%r{(?mi)(?x:
|
418
455
|
(?<!["'\[(\\])
|
419
|
-
((http|https)://)
|
420
|
-
([\w\-]+(
|
421
|
-
([\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
|
456
|
+
(?<protocol>(?:http|https)://)
|
457
|
+
(?<domain>[\w\-]+(?:\.[\w\-]+)+)
|
458
|
+
(?<path>[\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
|
422
459
|
)}) do |_match|
|
423
460
|
m = Regexp.last_match
|
424
|
-
|
425
|
-
|
461
|
+
url = "#{m['domain']}#{m['path']}"
|
462
|
+
proto = m['protocol'].nil? ? 'http://' : m['protocol']
|
463
|
+
case fmt
|
464
|
+
when :terminal
|
465
|
+
TTY::Link.link_to("#{proto}#{url}", "#{proto}#{url}")
|
426
466
|
when :html
|
427
|
-
%(<a href="#{proto}#{
|
467
|
+
%(<a href="#{proto}#{url}" title="Link to #{m['domain']}">[#{url}]</a>)
|
428
468
|
when :markdown
|
429
|
-
"[#{
|
469
|
+
"[#{url}](#{proto}#{url})"
|
430
470
|
else
|
431
471
|
m[0]
|
432
472
|
end
|
@@ -444,5 +484,41 @@ module Doing
|
|
444
484
|
end
|
445
485
|
end
|
446
486
|
end
|
487
|
+
|
488
|
+
def set_type(kind = nil)
|
489
|
+
if kind
|
490
|
+
case kind.to_s
|
491
|
+
when /^a/i
|
492
|
+
gsub(/^\[ *| *\]$/, '').split(/ *, */)
|
493
|
+
when /^i/i
|
494
|
+
to_i
|
495
|
+
when /^f/i
|
496
|
+
to_f
|
497
|
+
when /^sy/i
|
498
|
+
sub(/^:/, '').to_sym
|
499
|
+
when /^b/i
|
500
|
+
self =~ /^(true|yes)$/ ? true : false
|
501
|
+
else
|
502
|
+
to_s
|
503
|
+
end
|
504
|
+
else
|
505
|
+
case self
|
506
|
+
when / *, */
|
507
|
+
gsub(/^\[ *| *\]$/, '').split(/ *, */)
|
508
|
+
when /^[0-9]+$/
|
509
|
+
to_i
|
510
|
+
when /^[0-9]+\.[0-9]+$/
|
511
|
+
to_f
|
512
|
+
when /^:\w+/
|
513
|
+
sub(/^:/, '').to_sym
|
514
|
+
when /^(true|yes)$/i
|
515
|
+
true
|
516
|
+
when /^(false|no)$/i
|
517
|
+
false
|
518
|
+
else
|
519
|
+
to_s
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
447
523
|
end
|
448
524
|
end
|
data/lib/doing/util.rb
CHANGED
@@ -21,11 +21,17 @@ module Doing
|
|
21
21
|
def exec_available(cli)
|
22
22
|
return false if cli.nil?
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
!TTY::Which.which(cli).nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
## Return the first valid executable from a list of commands
|
29
|
+
##
|
30
|
+
## @example `Doing::Util.first_available_exec('bat', 'less -Xr', 'more -r', 'cat')`
|
31
|
+
##
|
32
|
+
def first_available_exec(*commands)
|
33
|
+
commands.compact.map(&:strip).reject(&:empty?).uniq
|
34
|
+
.find { |cmd| exec_available(cmd.split.first) }
|
29
35
|
end
|
30
36
|
|
31
37
|
def merge_default_proc(target, overwrite)
|
@@ -116,6 +122,7 @@ module Doing
|
|
116
122
|
|
117
123
|
File.open(file, 'w+') do |f|
|
118
124
|
f.puts content
|
125
|
+
Doing.logger.debug('Write:', "File written: #{file}")
|
119
126
|
end
|
120
127
|
|
121
128
|
Hooks.trigger :post_write, file
|
@@ -183,7 +190,8 @@ module Doing
|
|
183
190
|
Doing.logger.debug('ENV:', 'No EDITOR environment variable, testing available editors')
|
184
191
|
editors = %w[vim vi code subl mate mvim nano emacs]
|
185
192
|
editors.each do |ed|
|
186
|
-
return ed if
|
193
|
+
return TTY::Which.which(ed) if TTY::Which.which(ed)
|
194
|
+
|
187
195
|
Doing.logger.debug('ENV:', "#{ed} not available")
|
188
196
|
end
|
189
197
|
|
data/lib/doing/version.rb
CHANGED