doing 2.0.25 → 2.1.0pre
Sign up to get free protection for your applications and to get access to all the features.
- 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