doing 2.0.22 → 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 +36 -1
- data/Gemfile.lock +8 -1
- data/README.md +7 -1
- data/Rakefile +23 -4
- data/bin/doing +323 -173
- 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 +79 -27
- data/example_plugin.rb +5 -5
- data/lib/completion/_doing.zsh +42 -42
- data/lib/completion/doing.bash +10 -10
- data/lib/completion/doing.fish +1 -280
- data/lib/doing/array.rb +36 -0
- data/lib/doing/colors.rb +70 -66
- data/lib/doing/completion/bash_completion.rb +1 -2
- data/lib/doing/completion/fish_completion.rb +1 -1
- data/lib/doing/completion/zsh_completion.rb +1 -1
- data/lib/doing/completion.rb +6 -0
- data/lib/doing/configuration.rb +134 -23
- 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 +58 -4
- 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 +123 -35
- data/lib/doing/util.rb +14 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +307 -614
- data/lib/doing.rb +6 -2
- data/lib/examples/plugins/capture_thing_import.rb +162 -0
- data/rdoc_to_mmd.rb +14 -8
- data/scripts/generate_bash_completions.rb +1 -1
- data/scripts/generate_fish_completions.rb +1 -1
- data/scripts/generate_zsh_completions.rb +1 -1
- metadata +73 -5
- data/lib/doing/wwidfile.rb +0 -117
data/lib/doing/note.rb
CHANGED
@@ -5,12 +5,27 @@ module Doing
|
|
5
5
|
## This class describes an item note.
|
6
6
|
##
|
7
7
|
class Note < Array
|
8
|
+
|
9
|
+
##
|
10
|
+
## Initializes a new note
|
11
|
+
##
|
12
|
+
## @param note [Array] Initial note, can be string
|
13
|
+
## or array
|
14
|
+
##
|
8
15
|
def initialize(note = [])
|
9
16
|
super()
|
10
17
|
|
11
18
|
add(note) if note
|
12
19
|
end
|
13
20
|
|
21
|
+
##
|
22
|
+
## Add note contents, optionally replacing existing note
|
23
|
+
##
|
24
|
+
## @param note [Array] The note to add, can be
|
25
|
+
## string or array (Note)
|
26
|
+
## @param replace [Boolean] replace existing
|
27
|
+
## content
|
28
|
+
##
|
14
29
|
def add(note, replace: false)
|
15
30
|
clear if replace
|
16
31
|
if note.is_a?(String)
|
@@ -20,11 +35,22 @@ module Doing
|
|
20
35
|
end
|
21
36
|
end
|
22
37
|
|
38
|
+
##
|
39
|
+
## Append an array of strings to note
|
40
|
+
##
|
41
|
+
## @param lines [Array] Array of strings
|
42
|
+
##
|
23
43
|
def append(lines)
|
24
44
|
concat(lines)
|
25
45
|
replace compress
|
26
46
|
end
|
27
47
|
|
48
|
+
##
|
49
|
+
## Append a string to the note content
|
50
|
+
##
|
51
|
+
## @param input [String] The input string,
|
52
|
+
## newlines will be split
|
53
|
+
##
|
28
54
|
def append_string(input)
|
29
55
|
concat(input.split(/\n/).map(&:strip))
|
30
56
|
replace compress
|
@@ -34,6 +60,11 @@ module Doing
|
|
34
60
|
replace compress
|
35
61
|
end
|
36
62
|
|
63
|
+
##
|
64
|
+
## Remove blank lines and comment lines (#)
|
65
|
+
##
|
66
|
+
## @return [Array] compressed array
|
67
|
+
##
|
37
68
|
def compress
|
38
69
|
delete_if { |l| l =~ /^\s*$/ || l =~ /^#/ }
|
39
70
|
end
|
@@ -42,14 +73,35 @@ module Doing
|
|
42
73
|
replace strip_lines
|
43
74
|
end
|
44
75
|
|
76
|
+
##
|
77
|
+
## Remove leading/trailing whitespace for
|
78
|
+
## every line of note
|
79
|
+
##
|
80
|
+
## @return [Array] Stripped note
|
81
|
+
##
|
45
82
|
def strip_lines
|
46
83
|
map(&:strip)
|
47
84
|
end
|
48
85
|
|
86
|
+
##
|
87
|
+
## Note as multi-line string
|
49
88
|
def to_s
|
50
|
-
compress.strip_lines.join("\n")
|
89
|
+
compress.strip_lines.map { |l| "\t\t#{l}" }.join("\n")
|
90
|
+
end
|
91
|
+
|
92
|
+
# @private
|
93
|
+
def inspect
|
94
|
+
"<Doing::Note - characters:#{compress.strip_lines.join(' ').length} lines:#{count}>"
|
51
95
|
end
|
52
96
|
|
97
|
+
##
|
98
|
+
## Test if a note is equal (compare string
|
99
|
+
## representations)
|
100
|
+
##
|
101
|
+
## @param other [Note] The other Note
|
102
|
+
##
|
103
|
+
## @return [Boolean] true if equal
|
104
|
+
##
|
53
105
|
def equal?(other)
|
54
106
|
return false unless other.is_a?(Note)
|
55
107
|
|
data/lib/doing/pager.rb
CHANGED
@@ -1,23 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require 'pathname'
|
2
3
|
|
3
4
|
module Doing
|
4
5
|
# Pagination
|
5
6
|
module Pager
|
6
7
|
class << self
|
8
|
+
# Boolean determines whether output is paginated
|
7
9
|
def paginate
|
8
10
|
@paginate ||= false
|
9
11
|
end
|
10
12
|
|
13
|
+
# Enable/disable pagination
|
14
|
+
#
|
15
|
+
# @param should_paginate [Boolean] true to paginate
|
11
16
|
def paginate=(should_paginate)
|
12
17
|
@paginate = should_paginate
|
13
18
|
end
|
14
19
|
|
20
|
+
# Page output. If @paginate is false, just dump to
|
21
|
+
# STDOUT
|
22
|
+
#
|
23
|
+
# @param text [String] text to paginate
|
24
|
+
#
|
15
25
|
def page(text)
|
16
26
|
unless @paginate
|
17
27
|
puts text
|
18
28
|
return
|
19
29
|
end
|
20
30
|
|
31
|
+
pager = which_pager
|
32
|
+
Doing.logger.debug('Pager:', "Using #{pager}")
|
33
|
+
|
21
34
|
read_io, write_io = IO.pipe
|
22
35
|
|
23
36
|
input = $stdin
|
@@ -30,10 +43,8 @@ module Doing
|
|
30
43
|
# Wait until we have input before we start the pager
|
31
44
|
IO.select [input]
|
32
45
|
|
33
|
-
pager = which_pager
|
34
|
-
Doing.logger.debug('Pager:', "Using #{pager}")
|
35
46
|
begin
|
36
|
-
exec(pager
|
47
|
+
exec(pager)
|
37
48
|
rescue SystemCallError => e
|
38
49
|
raise Errors::DoingStandardError, "Pager error, #{e}"
|
39
50
|
end
|
@@ -51,44 +62,44 @@ module Doing
|
|
51
62
|
status.success?
|
52
63
|
end
|
53
64
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
if f.strip =~ /[ |]/
|
67
|
-
f
|
68
|
-
elsif f == 'most'
|
69
|
-
Doing.logger.warn('most not allowed as pager')
|
70
|
-
false
|
71
|
-
else
|
72
|
-
system "which #{f}", out: File::NULL, err: File::NULL
|
73
|
-
end
|
74
|
-
else
|
75
|
-
false
|
65
|
+
private
|
66
|
+
|
67
|
+
def command_exist?(command)
|
68
|
+
exts = ENV.fetch("PATHEXT", "").split(::File::PATH_SEPARATOR)
|
69
|
+
if Pathname.new(command).absolute?
|
70
|
+
::File.exist?(command) ||
|
71
|
+
exts.any? { |ext| ::File.exist?("#{command}#{ext}")}
|
72
|
+
else
|
73
|
+
ENV.fetch("PATH", "").split(::File::PATH_SEPARATOR).any? do |dir|
|
74
|
+
file = ::File.join(dir, command)
|
75
|
+
::File.exist?(file) ||
|
76
|
+
exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
|
76
77
|
end
|
77
78
|
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def git_pager
|
82
|
+
command_exist?("git") ? `git config --get-all core.pager` : nil
|
83
|
+
end
|
78
84
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
85
|
+
def pagers
|
86
|
+
[ENV['GIT_PAGER'], ENV['PAGER'], git_pager,
|
87
|
+
'bat -p --pager="less -Xr"', 'less -Xr', 'more -r'].compact
|
88
|
+
end
|
89
|
+
|
90
|
+
def find_executable(*commands)
|
91
|
+
execs = commands.empty? ? pagers : commands
|
92
|
+
execs
|
93
|
+
.compact.map(&:strip).reject(&:empty?).uniq
|
94
|
+
.find { |cmd| command_exist?(cmd.split.first) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def exec_available?(*commands)
|
98
|
+
!find_executable(*commands).nil?
|
99
|
+
end
|
100
|
+
|
101
|
+
def which_pager
|
102
|
+
@which_pager ||= find_executable(*pagers)
|
92
103
|
end
|
93
104
|
end
|
94
105
|
end
|
@@ -41,11 +41,11 @@ module Doing
|
|
41
41
|
all_items = []
|
42
42
|
items.each do |i|
|
43
43
|
if String.method_defined? :force_encoding
|
44
|
-
title = i.title.force_encoding('utf-8').link_urls(
|
45
|
-
note = i.note.map { |line| line.force_encoding('utf-8').strip.link_urls(
|
44
|
+
title = i.title.force_encoding('utf-8').link_urls(format: :markdown)
|
45
|
+
note = i.note.map { |line| line.force_encoding('utf-8').strip.link_urls(format: :markdown) } if i.note
|
46
46
|
else
|
47
|
-
title = i.title.link_urls(
|
48
|
-
note = i.note.map { |line| line.strip.link_urls(
|
47
|
+
title = i.title.link_urls(format: :markdown)
|
48
|
+
note = i.note.map { |line| line.strip.link_urls(format: :markdown) } if i.note
|
49
49
|
end
|
50
50
|
|
51
51
|
title = "#{title} @project(#{i.section})" unless variables[:is_single]
|
@@ -23,10 +23,10 @@ module Doing
|
|
23
23
|
out = ''
|
24
24
|
items.each do |item|
|
25
25
|
if opt[:highlight] && item.title =~ /@#{wwid.config['marker_tag']}\b/i
|
26
|
-
flag = Doing::Color.send(wwid.config['marker_color'])
|
26
|
+
# flag = Doing::Color.send(wwid.config['marker_color'])
|
27
27
|
reset = Doing::Color.default
|
28
28
|
else
|
29
|
-
flag = ''
|
29
|
+
# flag = ''
|
30
30
|
reset = ''
|
31
31
|
end
|
32
32
|
|
@@ -27,7 +27,7 @@ module Doing
|
|
27
27
|
options[:no_overlap] ||= false
|
28
28
|
options[:autotag] ||= wwid.auto_tag
|
29
29
|
|
30
|
-
wwid.add_section(section) unless wwid.content.
|
30
|
+
wwid.content.add_section(section) unless wwid.content.section?(section)
|
31
31
|
|
32
32
|
tags = options[:tag] ? options[:tag].split(/[ ,]+/).map { |t| t.sub(/^@?/, '') } : []
|
33
33
|
prefix = options[:prefix] || '[Calendar.app]'
|
@@ -59,7 +59,7 @@ module Doing
|
|
59
59
|
title += " @done(#{end_time.strftime('%Y-%m-%d %H:%M')})"
|
60
60
|
title.gsub!(/ +/, ' ')
|
61
61
|
title.strip!
|
62
|
-
new_entry =
|
62
|
+
new_entry = Item.new(start_time, title, section)
|
63
63
|
new_entry.note = entry['notes'].split(/\n/).map(&:chomp) if entry.key?('notes')
|
64
64
|
new_items.push(new_entry)
|
65
65
|
end
|
@@ -69,11 +69,11 @@ module Doing
|
|
69
69
|
filtered = total - new_items.count
|
70
70
|
Doing.logger.debug('Skipped:' , %(#{filtered} items that didn't match filter criteria)) if filtered.positive?
|
71
71
|
|
72
|
-
new_items = wwid.dedup(new_items, options[:no_overlap])
|
72
|
+
new_items = wwid.dedup(new_items, no_overlap: options[:no_overlap])
|
73
73
|
dups = filtered - new_items.count
|
74
74
|
Doing.logger.info(%(Skipped #{dups} items with overlapping times)) if dups.positive?
|
75
75
|
|
76
|
-
wwid.content
|
76
|
+
wwid.content.concat(new_items)
|
77
77
|
Doing.logger.info(%(Imported #{new_items.count} items to #{section}))
|
78
78
|
end
|
79
79
|
|
@@ -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
|