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
data/lib/doing/hash.rb
CHANGED
@@ -27,5 +27,42 @@ module Doing
|
|
27
27
|
def symbolize_keys
|
28
28
|
each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
|
29
29
|
end
|
30
|
+
|
31
|
+
# Set a nested hash value using an array
|
32
|
+
#
|
33
|
+
# @example `{}.deep_set(['one', 'two'], 'value')`
|
34
|
+
# @example `=> { 'one' => { 'two' => 'value' } }
|
35
|
+
#
|
36
|
+
# @param path [Array] key path
|
37
|
+
# @param value The value
|
38
|
+
#
|
39
|
+
def deep_set(path, value)
|
40
|
+
if path.count == 1
|
41
|
+
if value
|
42
|
+
self[path[0]] = value
|
43
|
+
else
|
44
|
+
delete(path[0])
|
45
|
+
end
|
46
|
+
else
|
47
|
+
if value
|
48
|
+
self.default_proc = ->(h, k) { h[k] = Hash.new(&h.default_proc) }
|
49
|
+
dig(*path[0..-2])[path.fetch(-1)] = value
|
50
|
+
else
|
51
|
+
return self unless dig(*path)
|
52
|
+
|
53
|
+
dig(*path[0..-2]).delete(path.fetch(-1))
|
54
|
+
path.pop
|
55
|
+
cleaned = self
|
56
|
+
path.each do |key|
|
57
|
+
if cleaned[key].empty?
|
58
|
+
cleaned.delete(key)
|
59
|
+
break
|
60
|
+
end
|
61
|
+
cleaned = cleaned[key]
|
62
|
+
end
|
63
|
+
empty? ? nil : self
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
30
67
|
end
|
31
68
|
end
|
data/lib/doing/item.rb
CHANGED
@@ -5,10 +5,10 @@ module Doing
|
|
5
5
|
## This class describes a single WWID item
|
6
6
|
##
|
7
7
|
class Item
|
8
|
-
# include Amatch
|
9
|
-
|
10
8
|
attr_accessor :date, :title, :section, :note
|
11
9
|
|
10
|
+
attr_reader :id
|
11
|
+
|
12
12
|
##
|
13
13
|
## Initialize an item with date, title, section, and
|
14
14
|
## optional note
|
@@ -50,8 +50,11 @@ module Doing
|
|
50
50
|
@end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
|
51
51
|
end
|
52
52
|
|
53
|
-
|
54
|
-
|
53
|
+
# Generate a hash that represents the entry
|
54
|
+
#
|
55
|
+
# @return [String] entry hash
|
56
|
+
def id
|
57
|
+
@id ||= (@date.to_s + @title + @section).hash
|
55
58
|
end
|
56
59
|
|
57
60
|
##
|
@@ -105,14 +108,42 @@ module Doing
|
|
105
108
|
##
|
106
109
|
## Add (or remove) tags from the title of the item
|
107
110
|
##
|
108
|
-
## @param
|
109
|
-
## @param
|
110
|
-
##
|
111
|
-
## @
|
112
|
-
## @
|
111
|
+
## @param tags [Array] The tags to apply
|
112
|
+
## @param **options Additional options
|
113
|
+
##
|
114
|
+
## @option options :date [Boolean] Include timestamp?
|
115
|
+
## @option options :single [Boolean] Log as a single change?
|
116
|
+
## @option options :value [String] A value to include as @tag(value)
|
117
|
+
## @option options :remove [Boolean] if true remove instead of adding
|
118
|
+
## @option options :rename_to [String] if not nil, rename target tag to this tag name
|
119
|
+
## @option options :regex [Boolean] treat target tag string as regex pattern
|
120
|
+
## @option options :force [Boolean] with rename_to, add tag if it doesn't exist
|
113
121
|
##
|
114
|
-
def tag(
|
115
|
-
|
122
|
+
def tag(tags, **options)
|
123
|
+
added = []
|
124
|
+
removed = []
|
125
|
+
|
126
|
+
date = options.fetch(:date, false)
|
127
|
+
options[:value] ||= date ? Time.now.strftime('%F %R') : nil
|
128
|
+
options.delete(:date)
|
129
|
+
|
130
|
+
single = options.fetch(:single, false)
|
131
|
+
options.delete(:single)
|
132
|
+
|
133
|
+
tags = tags.to_tags if tags.is_a? ::String
|
134
|
+
|
135
|
+
remove = options.fetch(:remove, false)
|
136
|
+
tags.each do |tag|
|
137
|
+
bool = remove ? :and : :not
|
138
|
+
if tags?(tag, bool)
|
139
|
+
@title.tag!(tag, **options).strip!
|
140
|
+
remove ? removed.push(tag) : added.push(tag)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)
|
145
|
+
|
146
|
+
self
|
116
147
|
end
|
117
148
|
|
118
149
|
##
|
@@ -121,7 +152,7 @@ module Doing
|
|
121
152
|
## @return [Array] array of tags (no values)
|
122
153
|
##
|
123
154
|
def tags
|
124
|
-
@title.scan(/(?<= |\A)@([^\s(]+)/).map {|tag| tag[0]}.sort.uniq
|
155
|
+
@title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
|
125
156
|
end
|
126
157
|
|
127
158
|
##
|
@@ -190,6 +221,40 @@ module Doing
|
|
190
221
|
should?('never_time')
|
191
222
|
end
|
192
223
|
|
224
|
+
##
|
225
|
+
## Move item from current section to destination section
|
226
|
+
##
|
227
|
+
## @param new_section [String] The destination
|
228
|
+
## section
|
229
|
+
## @param label [Boolean] add @from(original
|
230
|
+
## section) tag
|
231
|
+
## @param log [Boolean] log this action
|
232
|
+
##
|
233
|
+
## @return nothing
|
234
|
+
##
|
235
|
+
def move_to(new_section, label: true, log: true)
|
236
|
+
from = @section
|
237
|
+
|
238
|
+
tag('from', rename_to: 'from', value: from, force: true) if label
|
239
|
+
@section = new_section
|
240
|
+
|
241
|
+
Doing.logger.count(@section == 'Archive' ? :archived : :moved) if log
|
242
|
+
Doing.logger.debug("#{@section == 'Archive' ? 'Archived' : 'Moved'}:",
|
243
|
+
"#{@title.truncate(60)} from #{from} to #{@section}")
|
244
|
+
self
|
245
|
+
end
|
246
|
+
|
247
|
+
# outputs item in Doing file format, including leading tab
|
248
|
+
def to_s
|
249
|
+
"\t- #{@date.strftime('%Y-%m-%d %H:%M')} | #{@title}#{@note.empty? ? '' : "\n#{@note}"}"
|
250
|
+
end
|
251
|
+
|
252
|
+
# @private
|
253
|
+
def inspect
|
254
|
+
# %(<Doing::Item @date=#{@date} @title="#{@title}" @section:"#{@section}" @note:#{@note.to_s}>)
|
255
|
+
%(<Doing::Item @date=#{@date}>)
|
256
|
+
end
|
257
|
+
|
193
258
|
private
|
194
259
|
|
195
260
|
def should?(key)
|
data/lib/doing/items.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
# Items Array
|
5
|
+
class Items < Array
|
6
|
+
attr_accessor :sections
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@sections = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# List sections, title only
|
14
|
+
#
|
15
|
+
# @return [Array] section titles
|
16
|
+
#
|
17
|
+
def section_titles
|
18
|
+
@sections.map(&:title)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Test if section already exists
|
22
|
+
#
|
23
|
+
# @param section [String] section title
|
24
|
+
#
|
25
|
+
# @return [Boolean] true if section exists
|
26
|
+
#
|
27
|
+
def section?(section)
|
28
|
+
has_section = false
|
29
|
+
section = section.is_a?(Section) ? section.title.downcase : section.downcase
|
30
|
+
@sections.each do |s|
|
31
|
+
if s.title.downcase == section
|
32
|
+
has_section = true
|
33
|
+
break
|
34
|
+
end
|
35
|
+
end
|
36
|
+
has_section
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add a new section to the sections array. Accepts
|
40
|
+
# either a Section object, or a title string that will
|
41
|
+
# be converted into a Section.
|
42
|
+
#
|
43
|
+
# @param section [Section] The section to add. A
|
44
|
+
# String value will be converted to
|
45
|
+
# Section automatically.
|
46
|
+
# @param log [Boolean] Add a log message
|
47
|
+
# notifying the user about the
|
48
|
+
# creation of the section.
|
49
|
+
#
|
50
|
+
# @return nothing
|
51
|
+
#
|
52
|
+
def add_section(section, log: false)
|
53
|
+
section = section.is_a?(Section) ? section : Section.new(section.cap_first)
|
54
|
+
|
55
|
+
return if section?(section)
|
56
|
+
|
57
|
+
@sections.push(section)
|
58
|
+
Doing.logger.info('New section:', %("#{section}" added)) if log
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get a new Items object containing only items in a
|
62
|
+
# specified section
|
63
|
+
#
|
64
|
+
# @param section [String] section title
|
65
|
+
#
|
66
|
+
# @return [Items] Array of items
|
67
|
+
#
|
68
|
+
def in_section(section)
|
69
|
+
if section =~ /^all$/i
|
70
|
+
dup
|
71
|
+
else
|
72
|
+
items = Items.new.concat(select { |item| item.section == section })
|
73
|
+
items.add_section(section, log: false)
|
74
|
+
items
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
## Delete an item from the index
|
80
|
+
##
|
81
|
+
## @param item The item
|
82
|
+
##
|
83
|
+
def delete_item(item, single: false)
|
84
|
+
deleted = delete(item)
|
85
|
+
Doing.logger.count(:deleted)
|
86
|
+
Doing.logger.info('Entry deleted:', deleted.title) if single
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
## Update an item in the index with a modified item
|
91
|
+
##
|
92
|
+
## @param old_item The old item
|
93
|
+
## @param new_item The new item
|
94
|
+
##
|
95
|
+
def update_item(old_item, new_item)
|
96
|
+
s_idx = index { |item| item.equal?(old_item) }
|
97
|
+
|
98
|
+
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
99
|
+
|
100
|
+
return if fetch(s_idx).equal?(new_item)
|
101
|
+
|
102
|
+
self[s_idx] = new_item
|
103
|
+
Doing.logger.count(:updated)
|
104
|
+
Doing.logger.info('Entry updated:', self[s_idx].title.truncate(60))
|
105
|
+
new_item
|
106
|
+
end
|
107
|
+
|
108
|
+
# Output sections and items in Doing file format
|
109
|
+
def to_s
|
110
|
+
out = []
|
111
|
+
@sections.each do |section|
|
112
|
+
out.push(section.original)
|
113
|
+
in_section(section.title).each { |item| out.push(item.to_s)}
|
114
|
+
end
|
115
|
+
|
116
|
+
out.join("\n")
|
117
|
+
end
|
118
|
+
|
119
|
+
# @private
|
120
|
+
def inspect
|
121
|
+
"#<Doing::Items #{count} items, #{@sections.count} sections: #{@sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')}>"
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
data/lib/doing/log_adapter.rb
CHANGED
@@ -5,9 +5,16 @@ module Doing
|
|
5
5
|
## Log adapter
|
6
6
|
##
|
7
7
|
class LogAdapter
|
8
|
-
|
8
|
+
# Sets the log device
|
9
|
+
attr_writer :logdev
|
9
10
|
|
10
|
-
|
11
|
+
# Max length of log messages (truncate in middle)
|
12
|
+
attr_writer :max_length
|
13
|
+
|
14
|
+
# Returns the current log level (debug, info, warn, error)
|
15
|
+
attr_reader :level
|
16
|
+
|
17
|
+
attr_reader :messages, :results
|
11
18
|
|
12
19
|
TOPIC_WIDTH = 12
|
13
20
|
|
@@ -28,6 +35,7 @@ module Doing
|
|
28
35
|
deleted
|
29
36
|
moved
|
30
37
|
removed_tags
|
38
|
+
rotated
|
31
39
|
skipped
|
32
40
|
updated
|
33
41
|
].freeze
|
@@ -45,6 +53,7 @@ module Doing
|
|
45
53
|
@logdev = $stderr
|
46
54
|
@max_length = `tput cols`.strip.to_i - 5 || 85
|
47
55
|
self.log_level = level
|
56
|
+
@prev_level = level
|
48
57
|
end
|
49
58
|
|
50
59
|
#
|
@@ -71,6 +80,22 @@ module Doing
|
|
71
80
|
@level = level
|
72
81
|
end
|
73
82
|
|
83
|
+
# Set log level temporarily
|
84
|
+
def temp_level(level)
|
85
|
+
return if level.nil? || level.to_sym == @log_level
|
86
|
+
|
87
|
+
@prev_level = log_level.dup
|
88
|
+
@log_level = level.to_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
# Restore temporary level
|
92
|
+
def restore_level
|
93
|
+
return if @prev_level.nil? || @prev_level == @log_level
|
94
|
+
|
95
|
+
self.log_level = @prev_level
|
96
|
+
@prev_level = nil
|
97
|
+
end
|
98
|
+
|
74
99
|
def adjust_verbosity(options = {})
|
75
100
|
if options[:log_level]
|
76
101
|
self.log_level = options[:log_level].to_sym
|
@@ -229,7 +254,6 @@ module Doing
|
|
229
254
|
##
|
230
255
|
def output_results
|
231
256
|
total_counters
|
232
|
-
|
233
257
|
results = @results.select { |msg| write_message?(msg[:level]) }.uniq
|
234
258
|
|
235
259
|
if @logdev == $stdout
|
@@ -241,10 +265,38 @@ module Doing
|
|
241
265
|
end
|
242
266
|
end
|
243
267
|
|
268
|
+
def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
|
269
|
+
if tags_added.empty? && tags_removed.empty?
|
270
|
+
count(:skipped, level: :debug, message: '%count %items with no change', count: count)
|
271
|
+
else
|
272
|
+
if tags_added.empty?
|
273
|
+
count(:skipped, level: :debug, message: 'no tags added to %count %items')
|
274
|
+
elsif single && item
|
275
|
+
added = tags_added.log_tags
|
276
|
+
info('Tagged:',
|
277
|
+
%(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{added} to #{item.title}))
|
278
|
+
else
|
279
|
+
count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
|
280
|
+
end
|
281
|
+
|
282
|
+
if tags_removed.empty?
|
283
|
+
count(:skipped, level: :debug, message: 'no tags removed from %count %items')
|
284
|
+
elsif single && item
|
285
|
+
added = tags_added.log_tags
|
286
|
+
info('Untagged:',
|
287
|
+
%(removed #{tags_removed.count == 1 ? 'tag' : 'tags'} #{added} from #{item.title}))
|
288
|
+
else
|
289
|
+
count(:removed_tags, level: :info, tag: tags_removed, message: '%tags removed from %count %items')
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
244
294
|
private
|
245
295
|
|
246
296
|
def format_counter(key, data)
|
247
297
|
case key
|
298
|
+
when :rotated
|
299
|
+
['Rotated:', data[:message] || 'rotated %count %items']
|
248
300
|
when :autotag
|
249
301
|
['Autotag:', data[:message] || 'autotagged %count %items']
|
250
302
|
when :added_tags
|
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
|
|