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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +18 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +36 -1
  6. data/Gemfile.lock +8 -1
  7. data/README.md +7 -1
  8. data/Rakefile +23 -4
  9. data/bin/doing +323 -173
  10. data/doc/Array.html +354 -1
  11. data/doc/Doing/Color.html +104 -92
  12. data/doc/Doing/Completion.html +216 -0
  13. data/doc/Doing/Configuration.html +340 -5
  14. data/doc/Doing/Content.html +229 -0
  15. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  16. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  17. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  18. data/doc/Doing/Errors/EmptyInput.html +1 -1
  19. data/doc/Doing/Errors/NoResults.html +1 -1
  20. data/doc/Doing/Errors/PluginException.html +1 -1
  21. data/doc/Doing/Errors/UserCancelled.html +1 -1
  22. data/doc/Doing/Errors/WrongCommand.html +1 -1
  23. data/doc/Doing/Errors.html +1 -1
  24. data/doc/Doing/Hooks.html +1 -1
  25. data/doc/Doing/Item.html +337 -49
  26. data/doc/Doing/Items.html +444 -35
  27. data/doc/Doing/LogAdapter.html +139 -51
  28. data/doc/Doing/Note.html +253 -22
  29. data/doc/Doing/Pager.html +74 -36
  30. data/doc/Doing/Plugins.html +1 -1
  31. data/doc/Doing/Prompt.html +674 -0
  32. data/doc/Doing/Section.html +354 -0
  33. data/doc/Doing/Util.html +57 -1
  34. data/doc/Doing/WWID.html +477 -670
  35. data/doc/Doing/WWIDFile.html +398 -0
  36. data/doc/Doing.html +5 -5
  37. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  38. data/doc/GLI/Commands.html +1 -1
  39. data/doc/GLI.html +1 -1
  40. data/doc/Hash.html +97 -1
  41. data/doc/Status.html +37 -3
  42. data/doc/String.html +599 -23
  43. data/doc/Symbol.html +3 -3
  44. data/doc/Time.html +1 -1
  45. data/doc/_index.html +22 -1
  46. data/doc/class_list.html +1 -1
  47. data/doc/file.README.html +8 -2
  48. data/doc/index.html +8 -2
  49. data/doc/method_list.html +453 -173
  50. data/doc/top-level-namespace.html +1 -1
  51. data/doing.gemspec +3 -0
  52. data/doing.rdoc +79 -27
  53. data/example_plugin.rb +5 -5
  54. data/lib/completion/_doing.zsh +42 -42
  55. data/lib/completion/doing.bash +10 -10
  56. data/lib/completion/doing.fish +1 -280
  57. data/lib/doing/array.rb +36 -0
  58. data/lib/doing/colors.rb +70 -66
  59. data/lib/doing/completion/bash_completion.rb +1 -2
  60. data/lib/doing/completion/fish_completion.rb +1 -1
  61. data/lib/doing/completion/zsh_completion.rb +1 -1
  62. data/lib/doing/completion.rb +6 -0
  63. data/lib/doing/configuration.rb +134 -23
  64. data/lib/doing/hash.rb +37 -0
  65. data/lib/doing/item.rb +77 -12
  66. data/lib/doing/items.rb +125 -0
  67. data/lib/doing/log_adapter.rb +58 -4
  68. data/lib/doing/note.rb +53 -1
  69. data/lib/doing/pager.rb +49 -38
  70. data/lib/doing/plugins/export/markdown_export.rb +4 -4
  71. data/lib/doing/plugins/export/template_export.rb +2 -2
  72. data/lib/doing/plugins/import/calendar_import.rb +4 -4
  73. data/lib/doing/plugins/import/doing_import.rb +5 -7
  74. data/lib/doing/plugins/import/timing_import.rb +3 -3
  75. data/lib/doing/prompt.rb +206 -0
  76. data/lib/doing/section.rb +30 -0
  77. data/lib/doing/string.rb +123 -35
  78. data/lib/doing/util.rb +14 -6
  79. data/lib/doing/version.rb +1 -1
  80. data/lib/doing/wwid.rb +307 -614
  81. data/lib/doing.rb +6 -2
  82. data/lib/examples/plugins/capture_thing_import.rb +162 -0
  83. data/rdoc_to_mmd.rb +14 -8
  84. data/scripts/generate_bash_completions.rb +1 -1
  85. data/scripts/generate_fish_completions.rb +1 -1
  86. data/scripts/generate_zsh_completions.rb +1 -1
  87. metadata +73 -5
  88. 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.join(' '))
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
- def which_pager
55
- pagers = [ENV['GIT_PAGER'], ENV['PAGER']]
56
-
57
- if Util.exec_available('git')
58
- git_pager = `git config --get-all core.pager || true`.split.first
59
- git_pager && pagers.push(git_pager)
60
- end
61
-
62
- pagers.concat(%w[bat less more pager])
63
-
64
- pagers.select! do |f|
65
- if f
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
- pg = pagers.first
80
- args = case pg
81
- when /^more$/
82
- ' -r'
83
- when /^less$/
84
- ' -Xr'
85
- when /^bat$/
86
- ' -p --pager="less -Xr"'
87
- else
88
- ''
89
- end
90
-
91
- [pg, args]
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({format: :markdown})
45
- note = i.note.map { |line| line.force_encoding('utf-8').strip.link_urls({format: :markdown}) } if i.note
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({format: :markdown})
48
- note = i.note.map { |line| line.strip.link_urls({format: :markdown}) } if i.note
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.key?(section)
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 = { 'title' => title, 'date' => start_time, 'section' => section }
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[section][:items].concat(new_items)
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:' , %(#{skipped} items that didn't match filter criteria)) if skipped.positive?
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.key?(item.section)
85
- wwid.content[item.section][:items].push(item)
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.key?(section)
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[section][:items].concat(new_items)
80
+ wwid.content.concat(new_items)
81
81
  Doing.logger.info('Imported:', %(#{new_items.count} items to #{section}))
82
82
  end
83
83
 
@@ -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