doing 2.0.22 → 2.1.0pre

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