doing 2.1.97 → 2.1.101

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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +8 -2
  5. data/bin/commands/done.rb +8 -4
  6. data/bin/commands/finish.rb +1 -1
  7. data/bin/commands/reset.rb +3 -3
  8. data/bin/commands/view.rb +5 -0
  9. data/bin/doing +92 -2
  10. data/docs/doc/Array.html +3 -3
  11. data/docs/doc/BooleanTermParser/Clause.html +3 -3
  12. data/docs/doc/BooleanTermParser/Operator.html +3 -3
  13. data/docs/doc/BooleanTermParser/Query.html +3 -3
  14. data/docs/doc/BooleanTermParser/QueryParser.html +3 -3
  15. data/docs/doc/BooleanTermParser/QueryTransformer.html +3 -3
  16. data/docs/doc/BooleanTermParser.html +3 -3
  17. data/docs/doc/Doing/ArrayCleanup.html +3 -3
  18. data/docs/doc/Doing/ArrayNestedHash.html +3 -3
  19. data/docs/doc/Doing/ArrayTags.html +9 -9
  20. data/docs/doc/Doing/ByDayExport.html +3 -3
  21. data/docs/doc/Doing/CSVExport.html +3 -3
  22. data/docs/doc/Doing/CalendarImport.html +3 -3
  23. data/docs/doc/Doing/Change.html +3 -3
  24. data/docs/doc/Doing/Changes.html +3 -3
  25. data/docs/doc/Doing/ChronifyArray.html +3 -3
  26. data/docs/doc/Doing/ChronifyNumeric.html +3 -3
  27. data/docs/doc/Doing/ChronifyString.html +6 -6
  28. data/docs/doc/Doing/Color.html +16 -16
  29. data/docs/doc/Doing/Completion/BashCompletions.html +3 -3
  30. data/docs/doc/Doing/Completion/FigCompletions.html +3 -3
  31. data/docs/doc/Doing/Completion/FishCompletions.html +3 -3
  32. data/docs/doc/Doing/Completion/StringUtils.html +3 -3
  33. data/docs/doc/Doing/Completion/ZshCompletions.html +3 -3
  34. data/docs/doc/Doing/Completion.html +3 -3
  35. data/docs/doc/Doing/Configuration.html +3 -3
  36. data/docs/doc/Doing/DayOneRenderer.html +3 -3
  37. data/docs/doc/Doing/DayoneExport.html +3 -3
  38. data/docs/doc/Doing/DoingExport.html +3 -3
  39. data/docs/doc/Doing/DoingImport.html +3 -3
  40. data/docs/doc/Doing/Entry.html +3 -3
  41. data/docs/doc/Doing/Errors/DoingNoTraceError.html +3 -3
  42. data/docs/doc/Doing/Errors/DoingRuntimeError.html +3 -3
  43. data/docs/doc/Doing/Errors/DoingStandardError.html +3 -3
  44. data/docs/doc/Doing/Errors/EmptyInput.html +3 -3
  45. data/docs/doc/Doing/Errors/HistoryLimitError.html +3 -3
  46. data/docs/doc/Doing/Errors/InvalidPlugin.html +3 -3
  47. data/docs/doc/Doing/Errors/MissingBackupFile.html +3 -3
  48. data/docs/doc/Doing/Errors/NoResults.html +3 -3
  49. data/docs/doc/Doing/Errors/PluginException.html +3 -3
  50. data/docs/doc/Doing/Errors/UserCancelled.html +3 -3
  51. data/docs/doc/Doing/Errors/WrongCommand.html +3 -3
  52. data/docs/doc/Doing/Errors.html +3 -3
  53. data/docs/doc/Doing/HTMLExport.html +3 -3
  54. data/docs/doc/Doing/Hooks.html +3 -3
  55. data/docs/doc/Doing/Item.html +3 -3
  56. data/docs/doc/Doing/ItemDates.html +3 -3
  57. data/docs/doc/Doing/ItemQuery.html +3 -3
  58. data/docs/doc/Doing/ItemState.html +3 -3
  59. data/docs/doc/Doing/ItemTags.html +3 -3
  60. data/docs/doc/Doing/Items.html +3 -3
  61. data/docs/doc/Doing/JSONExport.html +3 -3
  62. data/docs/doc/Doing/JSONImport.html +3 -3
  63. data/docs/doc/Doing/Logger.html +3 -3
  64. data/docs/doc/Doing/MarkdownExport.html +3 -3
  65. data/docs/doc/Doing/Note.html +3 -3
  66. data/docs/doc/Doing/Pager.html +3 -3
  67. data/docs/doc/Doing/Plugins.html +3 -3
  68. data/docs/doc/Doing/Prompt.html +3 -3
  69. data/docs/doc/Doing/PromptChoose.html +3 -3
  70. data/docs/doc/Doing/PromptFZF.html +3 -3
  71. data/docs/doc/Doing/PromptInput.html +3 -3
  72. data/docs/doc/Doing/PromptSTD.html +3 -3
  73. data/docs/doc/Doing/PromptYN.html +3 -3
  74. data/docs/doc/Doing/Section.html +3 -3
  75. data/docs/doc/Doing/StringHighlight.html +3 -3
  76. data/docs/doc/Doing/StringNormalize.html +115 -3
  77. data/docs/doc/Doing/StringQuery.html +4 -4
  78. data/docs/doc/Doing/StringTags.html +3 -3
  79. data/docs/doc/Doing/StringTransform.html +3 -3
  80. data/docs/doc/Doing/StringTruncate.html +3 -3
  81. data/docs/doc/Doing/StringURL.html +3 -3
  82. data/docs/doc/Doing/SymbolNormalize.html +37 -3
  83. data/docs/doc/Doing/TaskPaperExport.html +3 -3
  84. data/docs/doc/Doing/TemplateExport.html +3 -3
  85. data/docs/doc/Doing/TemplateString.html +4 -4
  86. data/docs/doc/Doing/TimingImport.html +3 -3
  87. data/docs/doc/Doing/Types.html +13 -3
  88. data/docs/doc/Doing/Util/Backup.html +3 -3
  89. data/docs/doc/Doing/Util.html +4 -4
  90. data/docs/doc/Doing/Version.html +3 -3
  91. data/docs/doc/Doing/WWID.html +5 -5
  92. data/docs/doc/Doing.html +4 -4
  93. data/docs/doc/FalseClass.html +3 -3
  94. data/docs/doc/GLI/Commands/Help.html +3 -3
  95. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +3 -3
  96. data/docs/doc/GLI/Commands.html +3 -3
  97. data/docs/doc/GLI.html +3 -3
  98. data/docs/doc/Hash.html +4 -4
  99. data/docs/doc/Numeric.html +3 -3
  100. data/docs/doc/Object.html +3 -3
  101. data/docs/doc/PhraseParser/Operator.html +3 -3
  102. data/docs/doc/PhraseParser/PhraseClause.html +3 -3
  103. data/docs/doc/PhraseParser/Query.html +3 -3
  104. data/docs/doc/PhraseParser/QueryParser.html +3 -3
  105. data/docs/doc/PhraseParser/QueryTransformer.html +3 -3
  106. data/docs/doc/PhraseParser/TermClause.html +3 -3
  107. data/docs/doc/PhraseParser.html +3 -3
  108. data/docs/doc/Status.html +3 -3
  109. data/docs/doc/String.html +4 -4
  110. data/docs/doc/Symbol.html +4 -4
  111. data/docs/doc/Time.html +3 -3
  112. data/docs/doc/TrueClass.html +3 -3
  113. data/docs/doc/_index.html +4 -4
  114. data/docs/doc/class_list.html +3 -6
  115. data/docs/doc/css/full_list.css +3 -3
  116. data/docs/doc/css/style.css +15 -8
  117. data/docs/doc/file.README.html +13 -5
  118. data/docs/doc/file_list.html +2 -5
  119. data/docs/doc/frames.html +1 -1
  120. data/docs/doc/index.html +13 -5
  121. data/docs/doc/js/app.js +267 -348
  122. data/docs/doc/js/full_list.js +24 -52
  123. data/docs/doc/method_list.html +403 -382
  124. data/docs/doc/top-level-namespace.html +3 -3
  125. data/doing.rdoc +65 -1
  126. data/lib/completion/_doing.zsh +9 -9
  127. data/lib/completion/doing.bash +16 -16
  128. data/lib/completion/doing.fish +8 -0
  129. data/lib/completion/doing.ts +90 -0
  130. data/lib/doing/add_options.rb +4 -0
  131. data/lib/doing/chronify/string.rb +3 -3
  132. data/lib/doing/normalize.rb +25 -0
  133. data/lib/doing/plugins/export/dayone_export.rb +2 -1
  134. data/lib/doing/plugins/export/html_export.rb +1 -1
  135. data/lib/doing/plugins/export/json_export.rb +2 -1
  136. data/lib/doing/plugins/export/markdown_export.rb +2 -1
  137. data/lib/doing/plugins/export/template_export.rb +2 -1
  138. data/lib/doing/prompt/input.rb +8 -4
  139. data/lib/doing/prompt/yn.rb +2 -2
  140. data/lib/doing/types.rb +2 -0
  141. data/lib/doing/version.rb +1 -1
  142. data/lib/doing/wwid/timers.rb +146 -94
  143. data/lib/doing.rb +7 -1
  144. data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
  145. metadata +2 -2
@@ -161,6 +161,27 @@ module Doing
161
161
  replace normalize_bool(default)
162
162
  end
163
163
 
164
+ ##
165
+ ## Convert totals-by string to a symbol
166
+ ##
167
+ ## @return Symbol :tags or :section
168
+ ##
169
+ def normalize_totals_by(default = :tags)
170
+ case self
171
+ when /^t/i
172
+ :tags
173
+ when /^[sp]/i
174
+ :section
175
+ else
176
+ default.is_a?(Symbol) ? default : default.normalize_totals_by
177
+ end
178
+ end
179
+
180
+ ## @see #normalize_totals_by
181
+ def normalize_totals_by!(default = :tags)
182
+ replace normalize_totals_by(default)
183
+ end
184
+
164
185
  ##
165
186
  ## Adds ?: to any parentheticals in a regular expression
166
187
  ## to avoid match groups
@@ -217,6 +238,10 @@ module Doing
217
238
  def normalize_matching(default = :pattern)
218
239
  to_s.normalize_matching(default)
219
240
  end
241
+
242
+ def normalize_totals_by(default = :tags)
243
+ to_s.normalize_totals_by(default)
244
+ end
220
245
  end
221
246
  end
222
247
 
@@ -127,7 +127,8 @@ module Doing
127
127
 
128
128
  totals = if opt[:totals]
129
129
  wwid.tag_times(format: :markdown, sort_by: opt[:sort_tags],
130
- sort_order: opt[:tag_order])
130
+ sort_order: opt[:tag_order],
131
+ by: opt[:by])
131
132
  else
132
133
  ''
133
134
  end
@@ -70,7 +70,7 @@ module Doing
70
70
  self.template('css')
71
71
  end
72
72
 
73
- totals = opt[:totals] ? wwid.tag_times(format: :html, sort_by: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
73
+ totals = opt[:totals] ? wwid.tag_times(format: :html, sort_by: opt[:sort_tags], sort_order: opt[:tag_order], by: opt[:by]) : ''
74
74
  engine = Haml::Engine.new(template)
75
75
  Doing.logger.debug('HTML Export:', "#{items_out.count} items output to HTML")
76
76
  @out = engine.render(Object.new,
@@ -104,7 +104,8 @@ module Doing
104
104
  'items' => items_out,
105
105
  'timers' => wwid.tag_times(format: :json,
106
106
  sort_by: opt[:sort_tags],
107
- sort_order: opt[:tag_order])
107
+ sort_order: opt[:tag_order],
108
+ by: opt[:by])
108
109
  })
109
110
  when 'timeline'
110
111
  template = <<~EOTEMPLATE
@@ -82,7 +82,8 @@ module Doing
82
82
 
83
83
  totals = if opt[:totals]
84
84
  wwid.tag_times(format: :markdown, sort_by: opt[:sort_tags],
85
- sort_order: opt[:tag_order])
85
+ sort_order: opt[:tag_order],
86
+ by: opt[:by])
86
87
  else
87
88
  ''
88
89
  end
@@ -140,7 +140,8 @@ module Doing
140
140
  if opt[:totals]
141
141
  out += wwid.tag_times(format: Doing.setting('timer_format').to_sym,
142
142
  sort_by: opt[:sort_tags],
143
- sort_order: opt[:tag_order])
143
+ sort_order: opt[:tag_order],
144
+ by: opt[:by])
144
145
  end
145
146
  out
146
147
  end
@@ -17,9 +17,10 @@ module Doing
17
17
  ## @deprecated Use {#read_line} instead
18
18
  ##
19
19
  def enter_text(prompt, default_response: '')
20
- $stdin.reopen('/dev/tty')
21
20
  return default_response if @default_answer
22
21
 
22
+ $stdin.reopen('/dev/tty')
23
+
23
24
  print "#{Color.yellow(prompt).sub(/:?$/, ':')} #{Color.reset}"
24
25
  $stdin.gets.strip
25
26
  end
@@ -39,9 +40,10 @@ module Doing
39
40
  ## @return [String] User input string
40
41
  ##
41
42
  def read_line(prompt: 'Enter text', completions: [], default_response: '')
42
- $stdin.reopen('/dev/tty')
43
43
  return default_response if @default_answer
44
44
 
45
+ $stdin.reopen('/dev/tty')
46
+
45
47
  unless completions.empty?
46
48
  completions.sort!
47
49
  comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
@@ -71,9 +73,10 @@ module Doing
71
73
  ## @return [String] Multi-line result, joined with newlines
72
74
  ##
73
75
  def read_lines(prompt: 'Enter text', completions: [], default_response: '')
74
- $stdin.reopen('/dev/tty')
75
76
  return default_response if @default_answer
76
77
 
78
+ $stdin.reopen('/dev/tty')
79
+
77
80
  completions.sort!
78
81
  comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
79
82
  Readline.completion_append_character = ' '
@@ -111,9 +114,10 @@ module Doing
111
114
  ##
112
115
  ## @deprecated Use {#read_lines} instead
113
116
  def request_lines(prompt: 'Enter text', default_response: '')
114
- $stdin.reopen('/dev/tty')
115
117
  return default_response if @default_answer
116
118
 
119
+ $stdin.reopen('/dev/tty')
120
+
117
121
  ask_note = []
118
122
  reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
119
123
  puts "#{Color.boldgreen(prompt.sub(/:?$/,
@@ -16,8 +16,6 @@ module Doing
16
16
  def yn(question, default_response: false)
17
17
  return @force_answer == :yes unless @force_answer.nil?
18
18
 
19
- $stdin.reopen('/dev/tty')
20
-
21
19
  default = if default_response.is_a?(String)
22
20
  default_response =~ /y/i ? true : false
23
21
  else
@@ -30,6 +28,8 @@ module Doing
30
28
  # if this isn't an interactive shell, answer default
31
29
  return default unless $stdout.isatty
32
30
 
31
+ $stdin.reopen('/dev/tty')
32
+
33
33
  # clear the buffer
34
34
  ARGV.length&.times do
35
35
  ARGV.shift
data/lib/doing/types.rb CHANGED
@@ -6,6 +6,7 @@ module Doing
6
6
  REGEX_TAG_SORT = /^(?:n(?:ame)?|t(?:ime)?)$/i.freeze
7
7
  REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i.freeze
8
8
  REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i.freeze
9
+ REGEX_TOTALS_BY = /^(?:t(?:ag(?:s)?)?|s(?:ec(?:tion|tions)?)?|p(?:roj(?:ect|ects)?)?)$/i.freeze
9
10
  REGEX_VALUE_QUERY = /^(?:!)?@?(?:\S+) +(?:!?[<>=][=*]?|[$*^]=) +(?:.*?)$/.freeze
10
11
  REGEX_CLOCK = '(?:\d{1,2}+(?::\d{1,2}+)?(?: *(?:am|pm))?|midnight|noon)'
11
12
  REGEX_TIME = /^#{REGEX_CLOCK}$/i.freeze
@@ -30,6 +31,7 @@ module Doing
30
31
  OrderSymbol = Class.new(Symbol)
31
32
  TagArray = Class.new(Array)
32
33
  TagSortSymbol = Class.new(Symbol)
34
+ TotalsBySymbol = Class.new(Symbol)
33
35
  TemplateName = Class.new(String)
34
36
  end
35
37
  end
data/lib/doing/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Doing
4
- VERSION = '2.1.97'
4
+ VERSION = '2.1.101'
5
5
  end
@@ -11,12 +11,12 @@ module Doing
11
11
  ## @param sort_by [Symbol] Sort by :name or :time
12
12
  ## @param sort_order [Symbol] The sort order (:asc or :desc)
13
13
  ##
14
- def tag_times(format: :text, sort_by: :time, sort_order: :asc)
15
- return '' if @timers.empty?
14
+ def tag_times(format: :text, sort_by: :time, sort_order: :asc, by: nil)
15
+ groupings = normalize_totals_groupings(by)
16
+ totals = collect_totals(groupings)
17
+ return '' if totals.empty?
16
18
 
17
- @timers.delete('meanwhile')
18
-
19
- timers_snapshot = @timers.dup
19
+ timers_snapshot = totals[:tags] || {}
20
20
 
21
21
  budgets = Doing.setting('budgets', {}) || {}
22
22
  budgets = budgets.transform_keys { |k| k.to_s.downcase }
@@ -44,24 +44,123 @@ module Doing
44
44
  budgets_total += remaining
45
45
  end
46
46
 
47
- max = @timers.keys.sort_by(&:length).reverse[0].length + 1
47
+ outputs = groupings.map do |group|
48
+ timer_data = totals[group]
49
+ next nil unless timer_data
50
+
51
+ render_totals_group(
52
+ format: format,
53
+ group: group,
54
+ timer_data: timer_data,
55
+ sort_by: sort_by,
56
+ sort_order: sort_order,
57
+ remaining_map: remaining_map,
58
+ budgets: budgets,
59
+ budgets_total: budgets_total,
60
+ budget_fmt: budget_fmt
61
+ )
62
+ end.compact
63
+
64
+ return '' if outputs.empty?
65
+ return outputs.first if format == :json && outputs.length == 1
66
+
67
+ format == :json ? outputs.to_h : outputs.join(format == :human ? "\n" : '')
68
+ end
69
+
70
+ ##
71
+ ## Gets the interval between entry's start
72
+ ## date and @done date
73
+ ##
74
+ ## @param item [Item] The entry
75
+ ## @param formatted [Boolean] Return human readable
76
+ ## time (default seconds)
77
+ ## @param record [Boolean] Add the interval to the
78
+ ## total for each tag
79
+ ##
80
+ ## @return Interval in seconds, or [d, h, m] array if
81
+ ## formatted is true. False if no end date or
82
+ ## interval is 0
83
+ ##
84
+ def get_interval(item, formatted: true, record: true)
85
+ if item.interval
86
+ seconds = item.interval
87
+ record_tag_times(item, seconds) if record
88
+ return seconds.positive? ? seconds : false unless formatted
89
+
90
+ return seconds.positive? ? seconds.time_string(format: :clock) : false
91
+ end
92
+
93
+ false
94
+ end
48
95
 
49
- total = @timers.delete('All')
96
+ private
50
97
 
51
- tags_data = @timers.delete_if { |_k, v| v.zero? }
52
- sorted_tags_data = if sort_by.normalize_tag_sort == :name
53
- tags_data.sort_by { |k, _v| k }
54
- else
55
- tags_data.sort_by { |_k, v| v }
56
- end
98
+ ##
99
+ ## Record times for item tags
100
+ ##
101
+ ## @param item [Item] The item to record
102
+ ##
103
+ def record_tag_times(item, seconds)
104
+ item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
105
+ return if @recorded_items.include?(item_hash)
106
+
107
+ @section_timers ||= {}
108
+ section = item.section.to_s.strip
109
+ section = 'Unknown' if section.empty?
110
+ @section_timers['All'] = @section_timers.fetch('All', 0) + seconds
111
+ @section_timers[section] = @section_timers.fetch(section, 0) + seconds
112
+
113
+ item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
114
+ k = m[0] == 'done' ? 'All' : m[0].downcase
115
+ if @timers.key?(k)
116
+ @timers[k] += seconds
117
+ else
118
+ @timers[k] = seconds
119
+ end
120
+ @recorded_items.push(item_hash)
121
+ end
122
+ end
123
+
124
+ def normalize_totals_groupings(by)
125
+ return [:tags] if by.nil? || (by.respond_to?(:empty?) && by.empty?)
126
+
127
+ Array(by).map { |v| v.normalize_totals_by(:tags) }.uniq
128
+ end
129
+
130
+ def collect_totals(groupings)
131
+ totals = {}
132
+ totals[:tags] = @timers.dup if groupings.include?(:tags) && @timers.good?
133
+ totals[:section] = (@section_timers || {}).dup if groupings.include?(:section) && @section_timers.good?
134
+ totals
135
+ end
136
+
137
+ def sort_totals_data(data, sort_by:, sort_order:)
138
+ sorted = if sort_by.normalize_tag_sort == :name
139
+ data.sort_by { |k, _v| k }
140
+ else
141
+ data.sort_by { |_k, v| v }
142
+ end
143
+ sorted.reverse! if sort_order.normalize_order == :asc
144
+ sorted
145
+ end
146
+
147
+ def render_totals_group(format:, group:, timer_data:, sort_by:, sort_order:, remaining_map:, budgets:, budgets_total:, budget_fmt:)
148
+ timer_data = timer_data.dup
149
+ timer_data.delete('meanwhile')
150
+
151
+ max = timer_data.keys.sort_by(&:length).reverse[0].length + 1
152
+ total = timer_data.delete('All').to_i
153
+ group_data = timer_data.delete_if { |_k, v| v.zero? }
154
+ sorted_data = sort_totals_data(group_data, sort_by: sort_by, sort_order: sort_order)
155
+
156
+ title = group == :section ? 'Section Totals' : 'Tag Totals'
157
+ label = group == :section ? 'section' : 'tag'
57
158
 
58
- sorted_tags_data.reverse! if sort_order.normalize_order == :asc
59
159
  case format
60
160
  when :html
61
-
62
161
  output = <<EOHEAD
63
162
  <table>
64
- <caption id="tagtotals">Tag Totals</caption>
163
+ <caption id="#{group}totals">#{title}</caption>
65
164
  <colgroup>
66
165
  <col style="text-align:left;"/>
67
166
  <col style="text-align:left;"/>
@@ -74,16 +173,16 @@ module Doing
74
173
  </thead>
75
174
  <tbody>
76
175
  EOHEAD
77
- sorted_tags_data.reverse.each do |k, v|
176
+ sorted_data.reverse.each do |k, v|
78
177
  next unless v.positive?
79
178
 
80
179
  budget_str = ''
81
- if remaining_map.key?(k) && remaining_map[k].positive?
180
+ if group == :tags && remaining_map.key?(k) && remaining_map[k].positive?
82
181
  budget_str = " (budget left #{budget_fmt.call(remaining_map[k])})"
83
182
  end
84
183
  output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}#{budget_str}</td></tr>\n"
85
184
  end
86
- tail = <<EOTAIL
185
+ output += <<EOTAIL
87
186
  <tr>
88
187
  <td style="text-align:left;" colspan="2"></td>
89
188
  </tr>
@@ -91,52 +190,54 @@ EOHEAD
91
190
  <tfoot>
92
191
  <tr>
93
192
  <td style="text-align:left;"><strong>Total</strong></td>
94
- <td style="text-align:left;">#{total.time_string(format: :clock)}#{" (total budgets left #{budget_fmt.call(budgets_total)})" if budgets_total.positive?}</td>
193
+ <td style="text-align:left;">#{total.time_string(format: :clock)}#{group == :tags && budgets_total.positive? ? " (total budgets left #{budget_fmt.call(budgets_total)})" : ''}</td>
95
194
  </tr>
96
195
  </tfoot>
97
196
  </table>
98
197
  EOTAIL
99
- output + tail
198
+ output
100
199
  when :markdown
101
- pad = sorted_tags_data.map { |k, _| k }.group_by(&:size).max.last[0].length
200
+ pad = sorted_data.map { |k, _| k }.group_by(&:size).max.last[0].length
102
201
  pad = 7 if pad < 7
103
202
  output = <<~EOHEADER
104
203
  | #{' ' * (pad - 7)}project | time |
105
204
  | #{'-' * (pad - 1)}: | :------- |
106
205
  EOHEADER
107
- sorted_tags_data.reverse.each do |k, v|
206
+ sorted_data.reverse.each do |k, v|
108
207
  next unless v.positive?
109
208
 
110
209
  budget_str = ''
111
- if remaining_map.key?(k) && remaining_map[k].positive?
210
+ if group == :tags && remaining_map.key?(k) && remaining_map[k].positive?
112
211
  budget_str = " (budget left #{budget_fmt.call(remaining_map[k])})"
113
212
  end
114
213
  output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)}#{budget_str} |\n"
115
214
  end
116
- tail = '[Tag Totals]'
117
- output + tail
215
+ output + "[#{title}]"
118
216
  when :json
119
217
  output = []
120
- sorted_tags_data.reverse.each do |k, v|
121
- output << {
122
- 'tag' => k,
218
+ sorted_data.reverse.each do |k, v|
219
+ row = {
220
+ label => k,
123
221
  'seconds' => v,
124
- 'formatted' => v.time_string(format: :clock),
125
- 'budget' => budgets[k],
126
- 'remaining' => remaining_map[k],
127
- 'remaining_formatted' => (remaining_map[k] && remaining_map[k].positive? ? budget_fmt.call(remaining_map[k]) : nil)
222
+ 'formatted' => v.time_string(format: :clock)
128
223
  }
224
+ if group == :tags
225
+ row['budget'] = budgets[k]
226
+ row['remaining'] = remaining_map[k]
227
+ row['remaining_formatted'] = (remaining_map[k] && remaining_map[k].positive? ? budget_fmt.call(remaining_map[k]) : nil)
228
+ end
229
+ output << row
129
230
  end
130
- output
231
+ [group, output]
131
232
  when :human
132
233
  output = []
133
- sorted_tags_data.reverse.each do |k, v|
234
+ sorted_data.reverse.each do |k, v|
134
235
  spacer = ''
135
236
  (max - k.length).times do
136
237
  spacer += ' '
137
238
  end
138
239
  line = "┃ #{spacer}#{k}:#{v.time_string(format: :hm)}"
139
- if remaining_map.key?(k) && remaining_map[k].positive?
240
+ if group == :tags && remaining_map.key?(k) && remaining_map[k].positive?
140
241
  line += " (budget left #{budget_fmt.call(remaining_map[k])})"
141
242
  end
142
243
  line += ' ┃'
@@ -144,7 +245,7 @@ EOTAIL
144
245
  end
145
246
 
146
247
  total_content = "┃ #{' ' * (max - 6)}total: #{total.time_string(format: :hm)}"
147
- total_content += " (total budgets left #{budget_fmt.call(budgets_total)})" if budgets_total.positive?
248
+ total_content += " (total budgets left #{budget_fmt.call(budgets_total)})" if group == :tags && budgets_total.positive?
148
249
  total_content += ' ┃'
149
250
  max_line_len = (output + [total_content]).map(&:length).max
150
251
 
@@ -154,8 +255,9 @@ EOTAIL
154
255
  end
155
256
  output = output.map { |l| pad_line.call(l) }
156
257
 
157
- header = '┏━━ Tag Totals '
158
- [(max_line_len - 16), 0].max.times { header += '━' }
258
+ header = "┏━━ #{title} "
259
+ # Keep top border width aligned with body/footer width.
260
+ [(max_line_len - title.length - 6), 0].max.times { header += '━' }
159
261
  header += '┓'
160
262
  footer = '┗'
161
263
  [(max_line_len - 2), 0].max.times { footer += '━' }
@@ -172,7 +274,7 @@ EOTAIL
172
274
  total_time = total.time_string(format: :hm)
173
275
  total_line = "┃ #{spacer}total: "
174
276
  total_line += total_time
175
- total_line += " (total budgets left #{budget_fmt.call(budgets_total)})" if budgets_total.positive?
277
+ total_line += " (total budgets left #{budget_fmt.call(budgets_total)})" if group == :tags && budgets_total.positive?
176
278
  total_line += ' ┃'
177
279
  total_line = pad_line.call(total_line)
178
280
  output += "\n#{total_line}"
@@ -180,74 +282,24 @@ EOTAIL
180
282
  output
181
283
  else
182
284
  output = []
183
- sorted_tags_data.reverse.each do |k, v|
285
+ sorted_data.reverse.each do |k, v|
184
286
  spacer = ''
185
287
  (max - k.length).times do
186
288
  spacer += ' '
187
289
  end
188
290
  line = "#{k}:#{spacer}#{v.time_string(format: :clock)}"
189
- if remaining_map.key?(k) && remaining_map[k].positive?
291
+ if group == :tags && remaining_map.key?(k) && remaining_map[k].positive?
190
292
  line += " (budget left #{budget_fmt.call(remaining_map[k])})"
191
293
  end
192
294
  output.push(line)
193
295
  end
194
296
 
195
- output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
297
+ output = output.empty? ? '' : "\n--- #{title} ---\n#{output.join("\n")}"
196
298
  output += "\n\nTotal tracked: #{total.time_string(format: :clock)}"
197
- if budgets_total.positive?
198
- output += " (total budgets left #{budget_fmt.call(budgets_total)})"
199
- end
299
+ output += " (total budgets left #{budget_fmt.call(budgets_total)})" if group == :tags && budgets_total.positive?
200
300
  output += "\n"
201
301
  output
202
302
  end
203
303
  end
204
-
205
- ##
206
- ## Gets the interval between entry's start
207
- ## date and @done date
208
- ##
209
- ## @param item [Item] The entry
210
- ## @param formatted [Boolean] Return human readable
211
- ## time (default seconds)
212
- ## @param record [Boolean] Add the interval to the
213
- ## total for each tag
214
- ##
215
- ## @return Interval in seconds, or [d, h, m] array if
216
- ## formatted is true. False if no end date or
217
- ## interval is 0
218
- ##
219
- def get_interval(item, formatted: true, record: true)
220
- if item.interval
221
- seconds = item.interval
222
- record_tag_times(item, seconds) if record
223
- return seconds.positive? ? seconds : false unless formatted
224
-
225
- return seconds.positive? ? seconds.time_string(format: :clock) : false
226
- end
227
-
228
- false
229
- end
230
-
231
- private
232
-
233
- ##
234
- ## Record times for item tags
235
- ##
236
- ## @param item [Item] The item to record
237
- ##
238
- def record_tag_times(item, seconds)
239
- item_hash = "#{item.date.strftime('%s')}#{item.title}#{item.section}"
240
- return if @recorded_items.include?(item_hash)
241
-
242
- item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
243
- k = m[0] == 'done' ? 'All' : m[0].downcase
244
- if @timers.key?(k)
245
- @timers[k] += seconds
246
- else
247
- @timers[k] = seconds
248
- end
249
- @recorded_items.push(item_hash)
250
- end
251
- end
252
304
  end
253
305
  end
data/lib/doing.rb CHANGED
@@ -9,7 +9,13 @@ require 'tempfile'
9
9
  require 'zlib'
10
10
  require 'base64'
11
11
  require 'plist'
12
- require 'readline'
12
+ begin
13
+ require 'readline'
14
+ rescue LoadError
15
+ # Ruby 4 builds may not ship native readline; use stdlib Reline API-compatible fallback.
16
+ require 'reline'
17
+ Readline = Reline unless defined?(Readline)
18
+ end
13
19
  require 'haml'
14
20
  require 'json'
15
21
  require 'logger'
@@ -73,7 +73,7 @@ module Doing
73
73
  self.template('wiki_css')
74
74
  end
75
75
 
76
- totals = opt[:totals] ? wwid.tag_times(format: :html, sort_by: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
76
+ totals = opt[:totals] ? wwid.tag_times(format: :html, sort_by: opt[:sort_tags], sort_order: opt[:tag_order], by: opt[:by]) : ''
77
77
  engine = Haml::Engine.new(template)
78
78
  Doing.logger.debug('Wiki Export:', "#{items_out.count} items output to #{variables[:page_title]} wiki page")
79
79
  @out = engine.render(Object.new,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doing
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.97
4
+ version: 2.1.101
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -944,7 +944,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
944
944
  - !ruby/object:Gem::Version
945
945
  version: '0'
946
946
  requirements: []
947
- rubygems_version: 4.0.3
947
+ rubygems_version: 4.0.6
948
948
  specification_version: 4
949
949
  summary: A command line tool for managing What Was I Doing reminders
950
950
  test_files: []