doing 2.0.22 → 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 +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
|