doing 2.1.93 → 2.1.97

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/bin/commands/budget.rb +85 -0
  6. data/bin/doing +1 -1
  7. data/docs/doc/Array.html +1 -1
  8. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  9. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  10. data/docs/doc/BooleanTermParser/Query.html +1 -1
  11. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  12. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  13. data/docs/doc/BooleanTermParser.html +1 -1
  14. data/docs/doc/Doing/ArrayCleanup.html +1 -1
  15. data/docs/doc/Doing/ArrayNestedHash.html +1 -1
  16. data/docs/doc/Doing/ArrayTags.html +1 -1
  17. data/docs/doc/Doing/ByDayExport.html +1 -1
  18. data/docs/doc/Doing/CSVExport.html +1 -1
  19. data/docs/doc/Doing/CalendarImport.html +1 -1
  20. data/docs/doc/Doing/Change.html +1 -1
  21. data/docs/doc/Doing/Changes.html +1 -1
  22. data/docs/doc/Doing/ChronifyArray.html +1 -1
  23. data/docs/doc/Doing/ChronifyNumeric.html +1 -1
  24. data/docs/doc/Doing/ChronifyString.html +1 -1
  25. data/docs/doc/Doing/Color.html +1 -1
  26. data/docs/doc/Doing/Completion/BashCompletions.html +1 -1
  27. data/docs/doc/Doing/Completion/FigCompletions.html +1 -1
  28. data/docs/doc/Doing/Completion/FishCompletions.html +1 -1
  29. data/docs/doc/Doing/Completion/StringUtils.html +1 -1
  30. data/docs/doc/Doing/Completion/ZshCompletions.html +1 -1
  31. data/docs/doc/Doing/Completion.html +1 -1
  32. data/docs/doc/Doing/Configuration.html +3 -1
  33. data/docs/doc/Doing/DayOneRenderer.html +1 -1
  34. data/docs/doc/Doing/DayoneExport.html +1 -1
  35. data/docs/doc/Doing/DoingExport.html +1 -1
  36. data/docs/doc/Doing/DoingImport.html +1 -1
  37. data/docs/doc/Doing/Entry.html +1 -1
  38. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  39. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  40. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  41. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  42. data/docs/doc/Doing/Errors/HistoryLimitError.html +1 -1
  43. data/docs/doc/Doing/Errors/InvalidPlugin.html +1 -1
  44. data/docs/doc/Doing/Errors/MissingBackupFile.html +1 -1
  45. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  46. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  47. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  48. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  49. data/docs/doc/Doing/Errors.html +1 -1
  50. data/docs/doc/Doing/HTMLExport.html +1 -1
  51. data/docs/doc/Doing/Hooks.html +1 -1
  52. data/docs/doc/Doing/Item.html +1 -1
  53. data/docs/doc/Doing/ItemDates.html +1 -1
  54. data/docs/doc/Doing/ItemQuery.html +1 -1
  55. data/docs/doc/Doing/ItemState.html +1 -1
  56. data/docs/doc/Doing/ItemTags.html +1 -1
  57. data/docs/doc/Doing/Items.html +1 -1
  58. data/docs/doc/Doing/JSONExport.html +1 -1
  59. data/docs/doc/Doing/JSONImport.html +1 -1
  60. data/docs/doc/Doing/Logger.html +1 -1
  61. data/docs/doc/Doing/MarkdownExport.html +1 -1
  62. data/docs/doc/Doing/Note.html +1 -1
  63. data/docs/doc/Doing/Pager.html +1 -1
  64. data/docs/doc/Doing/Plugins.html +1 -1
  65. data/docs/doc/Doing/Prompt.html +1 -1
  66. data/docs/doc/Doing/PromptChoose.html +1 -1
  67. data/docs/doc/Doing/PromptFZF.html +1 -1
  68. data/docs/doc/Doing/PromptInput.html +1 -1
  69. data/docs/doc/Doing/PromptSTD.html +1 -1
  70. data/docs/doc/Doing/PromptYN.html +1 -1
  71. data/docs/doc/Doing/Section.html +1 -1
  72. data/docs/doc/Doing/StringHighlight.html +1 -1
  73. data/docs/doc/Doing/StringNormalize.html +1 -1
  74. data/docs/doc/Doing/StringQuery.html +1 -1
  75. data/docs/doc/Doing/StringTags.html +1 -1
  76. data/docs/doc/Doing/StringTransform.html +1 -1
  77. data/docs/doc/Doing/StringTruncate.html +1 -1
  78. data/docs/doc/Doing/StringURL.html +1 -1
  79. data/docs/doc/Doing/SymbolNormalize.html +1 -1
  80. data/docs/doc/Doing/TaskPaperExport.html +1 -1
  81. data/docs/doc/Doing/TemplateExport.html +1 -1
  82. data/docs/doc/Doing/TemplateString.html +1 -1
  83. data/docs/doc/Doing/TimingImport.html +1 -1
  84. data/docs/doc/Doing/Types.html +1 -1
  85. data/docs/doc/Doing/Util/Backup.html +1 -1
  86. data/docs/doc/Doing/Util.html +1 -1
  87. data/docs/doc/Doing/Version.html +1 -1
  88. data/docs/doc/Doing/WWID.html +1 -1
  89. data/docs/doc/Doing.html +2 -2
  90. data/docs/doc/FalseClass.html +1 -1
  91. data/docs/doc/GLI/Commands/Help.html +1 -1
  92. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  93. data/docs/doc/GLI/Commands.html +1 -1
  94. data/docs/doc/GLI.html +1 -1
  95. data/docs/doc/Hash.html +1 -1
  96. data/docs/doc/Numeric.html +1 -1
  97. data/docs/doc/Object.html +1 -1
  98. data/docs/doc/PhraseParser/Operator.html +1 -1
  99. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  100. data/docs/doc/PhraseParser/Query.html +1 -1
  101. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  102. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  103. data/docs/doc/PhraseParser/TermClause.html +1 -1
  104. data/docs/doc/PhraseParser.html +1 -1
  105. data/docs/doc/Status.html +1 -1
  106. data/docs/doc/String.html +1 -1
  107. data/docs/doc/Symbol.html +1 -1
  108. data/docs/doc/Time.html +1 -1
  109. data/docs/doc/TrueClass.html +1 -1
  110. data/docs/doc/_index.html +1 -1
  111. data/docs/doc/file.README.html +2 -2
  112. data/docs/doc/index.html +2 -2
  113. data/docs/doc/top-level-namespace.html +1 -1
  114. data/doing.rdoc +17 -1
  115. data/lib/completion/_doing.zsh +4 -0
  116. data/lib/completion/doing.bash +11 -0
  117. data/lib/completion/doing.fish +2 -0
  118. data/lib/completion/doing.ts +14 -0
  119. data/lib/doing/configuration.rb +4 -2
  120. data/lib/doing/plugins/export/byday.rb +39 -0
  121. data/lib/doing/version.rb +1 -1
  122. data/lib/doing/wwid/timers.rb +82 -15
  123. metadata +2 -1
data/docs/doc/_index.html CHANGED
@@ -961,7 +961,7 @@
961
961
  </div>
962
962
 
963
963
  <div id="footer">
964
- Generated on Tue Feb 10 04:26:33 2026 by
964
+ Generated on Sat Feb 14 06:59:18 2026 by
965
965
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
966
966
  0.9.38 (ruby-3.4.4).
967
967
  </div>
@@ -71,7 +71,7 @@ you&#39;ve done.</strong></p>
71
71
 
72
72
  <!--README-->
73
73
 
74
- <p>The current version of <code>doing</code> is &lt;!--VER--&gt;2.1.90&lt;!--END VER--&gt;.</p>
74
+ <p>The current version of <code>doing</code> is &lt;!--VER--&gt;2.1.94&lt;!--END VER--&gt;.</p>
75
75
 
76
76
  <p>Find all of the documentation in the <a href="https://github.com/ttscoff/doing/wiki">doing wiki</a>.</p>
77
77
 
@@ -153,7 +153,7 @@ with changes. Please target the <code>develop</code> branch with pull requests.<
153
153
  </div></div>
154
154
 
155
155
  <div id="footer">
156
- Generated on Tue Feb 10 04:26:33 2026 by
156
+ Generated on Sat Feb 14 06:59:18 2026 by
157
157
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
158
158
  0.9.38 (ruby-3.4.4).
159
159
  </div>
data/docs/doc/index.html CHANGED
@@ -71,7 +71,7 @@ you&#39;ve done.</strong></p>
71
71
 
72
72
  <!--README-->
73
73
 
74
- <p>The current version of <code>doing</code> is &lt;!--VER--&gt;2.1.90&lt;!--END VER--&gt;.</p>
74
+ <p>The current version of <code>doing</code> is &lt;!--VER--&gt;2.1.94&lt;!--END VER--&gt;.</p>
75
75
 
76
76
  <p>Find all of the documentation in the <a href="https://github.com/ttscoff/doing/wiki">doing wiki</a>.</p>
77
77
 
@@ -153,7 +153,7 @@ with changes. Please target the <code>develop</code> branch with pull requests.<
153
153
  </div></div>
154
154
 
155
155
  <div id="footer">
156
- Generated on Tue Feb 10 04:26:33 2026 by
156
+ Generated on Sat Feb 14 06:59:18 2026 by
157
157
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
158
158
  0.9.38 (ruby-3.4.4).
159
159
  </div>
@@ -216,7 +216,7 @@
216
216
  </div>
217
217
 
218
218
  <div id="footer">
219
- Generated on Tue Feb 10 04:26:33 2026 by
219
+ Generated on Sat Feb 14 06:59:18 2026 by
220
220
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
221
221
  0.9.38 (ruby-3.4.4).
222
222
  </div>
data/doing.rdoc CHANGED
@@ -5,7 +5,7 @@ record of what you've been doing, complete with tag-based time tracking. The
5
5
  command line tool allows you to add entries, annotate with tags and notes, and
6
6
  view your entries with myriad options, with a focus on a "natural" language syntax.
7
7
 
8
- v2.1.93
8
+ v2.1.97
9
9
 
10
10
  === Global Options
11
11
  === --config_file arg
@@ -346,6 +346,22 @@ Autotag last entry (or entries) not marked @done
346
346
 
347
347
 
348
348
 
349
+ ==== Command: <tt>budget [TAG [AMOUNT]]</tt>
350
+ Set, list, and remove tag time budgets
351
+
352
+ Manage simple time budgets for tags.
353
+
354
+ Run without arguments to list configured budgets.
355
+
356
+ Use `doing budget TAG AMOUNT` to set a budget (e.g. `doing budget dev 100h`).
357
+
358
+ Use `doing budget TAG --remove` to delete a budget.
359
+ ===== Options
360
+ ===== -r|--remove
361
+ Delete specified tag budget
362
+
363
+
364
+
349
365
  ==== Command: <tt>cancel COUNT</tt>
350
366
  End last X entries with no time tracked
351
367
 
@@ -12,6 +12,7 @@ function _doing() {
12
12
  'archive:Move entries between sections'
13
13
  'move:Move entries between sections'
14
14
  'autotag:Autotag last entry or filtered entries'
15
+ 'budget:Set'
15
16
  'cancel:End last X entries with no time tracked'
16
17
  'changes:List recent changes in Doing'
17
18
  'changelog:List recent changes in Doing'
@@ -82,6 +83,9 @@ function _doing() {
82
83
  autotag)
83
84
  args=( "--bool[Boolean]:BOOLEAN:" {'(--count)-c','(-c)--count'}"[How many recent entries to autotag]:COUNT:" "--force[Don't ask permission to autotag all entries when count is 0]" {'(--interactive)-i','(-i)--interactive'}"[Select item(s) to tag from a menu of matching entries]" {'(--section)-s','(-s)--section'}"[Section]:SECTION_NAME:" "--search[Autotag entries matching search filter]:QUERY:" "--tag[Autotag the last X entries containing TAG]:TAG:" {'(--unfinished)-u','(-u)--unfinished'}"[Autotag last entry]" )
84
85
  ;;
86
+ budget)
87
+ args=( {'(--remove)-r','(-r)--remove'}"[Delete specified tag budget]" )
88
+ ;;
85
89
  cancel)
86
90
  args=( {'(--archive)-a','(-a)--archive'}"[Archive entries]" "--bool[Boolean used to combine multiple tags]:BOOLEAN:" "--case[Case sensitivity for search string matching [(c)ase-sensitive]:TYPE:" {'(--interactive)-i','(-i)--interactive'}"[Select item(s) to cancel from a menu of matching entries]" "--not[Cancel items that *don't* match search/tag filters]" {'(--section)-s','(-s)--section'}"[Section]:NAME:" "--search[Filter entries using a search query]:QUERY:" "--tag[Filter entries by tag]:TAG:" {'(--unfinished)-u','(-u)--unfinished'}"[Cancel last entry]" "--val[Perform a tag value query]:QUERY:" {'(--exact)-x','(-x)--exact'}"[Force exact search string matching]" )
87
91
  ;;
@@ -48,6 +48,16 @@ _doing_autotag() {
48
48
  fi
49
49
  }
50
50
 
51
+ _doing_budget() {
52
+
53
+ if [[ "$token" == --* ]]; then
54
+ COMPREPLY=( $( compgen -W '--remove' -- $token ) )
55
+ elif [[ "$token" == -* ]]; then
56
+ COMPREPLY=( $( compgen -W '-r --remove' -- $token ) )
57
+
58
+ fi
59
+ }
60
+
51
61
  _doing_cancel() {
52
62
 
53
63
  if [[ "$token" == --* ]]; then
@@ -456,6 +466,7 @@ _doing()
456
466
  if [[ $last =~ (again|resume) ]]; then _doing_again
457
467
  elif [[ $last =~ (archive|move) ]]; then _doing_archive
458
468
  elif [[ $last =~ (autotag) ]]; then _doing_autotag
469
+ elif [[ $last =~ (budget) ]]; then _doing_budget
459
470
  elif [[ $last =~ (cancel) ]]; then _doing_cancel
460
471
  elif [[ $last =~ (changes|changelog) ]]; then _doing_changes
461
472
  elif [[ $last =~ (completion) ]]; then _doing_completion
@@ -138,6 +138,7 @@ __fish_doing_complete_args tag
138
138
  complete -xc doing -n '__fish_doing_needs_command' -a 'again resume' -d Repeat\ last\ entry\ as\ new\ entry
139
139
  complete -xc doing -n '__fish_doing_needs_command' -a 'archive move' -d Move\ entries\ between\ sections
140
140
  complete -xc doing -n '__fish_doing_needs_command' -a 'autotag' -d Autotag\ last\ entry\ or\ filtered\ entries
141
+ complete -xc doing -n '__fish_doing_needs_command' -a 'budget' -d Set
141
142
  complete -xc doing -n '__fish_doing_needs_command' -a 'cancel' -d End\ last\ X\ entries\ with\ no\ time\ tracked
142
143
  complete -xc doing -n '__fish_doing_needs_command' -a 'changes changelog' -d List\ recent\ changes\ in\ Doing
143
144
  complete -xc doing -n '__fish_doing_needs_command' -a 'colors' -d List\ available\ color\ variables\ for\ configuration\ templates\ and\ views
@@ -214,6 +215,7 @@ complete -c doing -l section -s s -f -r -n '__fish_doing_using_command autotag'
214
215
  complete -c doing -l search -f -r -n '__fish_doing_using_command autotag' -d Autotag\ entries\ matching\ search\ filter
215
216
  complete -c doing -l tag -f -r -n '__fish_doing_using_command autotag' -d Autotag\ the\ last\ X\ entries\ containing\ TAG
216
217
  complete -c doing -l unfinished -s u -f -n '__fish_doing_using_command autotag' -d Autotag\ last\ entry
218
+ complete -c doing -l remove -s r -f -n '__fish_doing_using_command budget' -d Delete\ specified\ tag\ budget
217
219
  complete -c doing -l archive -s a -f -n '__fish_doing_using_command cancel' -d Archive\ entries
218
220
  complete -c doing -l bool -f -r -n '__fish_doing_using_command cancel' -d Boolean\ used\ to\ combine\ multiple\ tags
219
221
  complete -c doing -l case -f -r -n '__fish_doing_using_command cancel' -d Case\ sensitivity\ for\ search\ string\ matching\ \[\(c\)ase-sensitive
@@ -598,6 +598,20 @@ const completionSpec: Fig.Spec = {
598
598
 
599
599
  },
600
600
 
601
+ {
602
+ name: "budget",
603
+ description: "Set",
604
+ options: [
605
+ {
606
+ name: ["-r", "--remove"],
607
+ description: "Delete specified tag budget",
608
+
609
+ },
610
+
611
+ ],
612
+
613
+ },
614
+
601
615
  {
602
616
  name: "cancel",
603
617
  description: "End last X entries with no time tracked",
@@ -41,6 +41,8 @@ module Doing
41
41
  'never_finish' => [],
42
42
  'date_tags' => ['done', 'defer(?:red)?', 'waiting'],
43
43
 
44
+ 'budgets' => {},
45
+
44
46
  'timer_format' => 'text',
45
47
  'interval_format' => 'text',
46
48
 
@@ -175,8 +177,8 @@ module Doing
175
177
 
176
178
  return @config_file if @force_answer
177
179
 
178
- if @additional_configs&.count&.positive? || create
179
- choices = [@config_file].concat(@additional_configs)
180
+ if additional_configs&.count&.positive? || create
181
+ choices = [@config_file].concat(additional_configs)
180
182
  choices.push('Create a new .doingrc in the current directory') if create && !File.exist?('.doingrc')
181
183
  res = Doing::Prompt.choose_from(choices.uniq.sort.reverse,
182
184
  sorted: false,
@@ -28,6 +28,7 @@ module Doing
28
28
 
29
29
  totals = {}
30
30
  total = 0
31
+ tag_totals = Hash.new(0)
31
32
 
32
33
  days.each do |day, day_items|
33
34
  day_items.each do |item|
@@ -35,8 +36,40 @@ module Doing
35
36
  duration = item.interval || 0
36
37
  totals[day] += duration
37
38
  total += duration
39
+
40
+ item.title.scan(/(?mi)@(\S+?)(\(.*\))?(?=\s|$)/).each do |m|
41
+ tag = m[0].downcase
42
+ next if tag == 'done'
43
+
44
+ tag_totals[tag] += duration
45
+ end
38
46
  end
39
47
  end
48
+
49
+ budgets = Doing.setting('budgets', {}) || {}
50
+ budgets = budgets.transform_keys { |k| k.to_s.downcase }
51
+ budgets_total = 0
52
+
53
+ budget_fmt = lambda do |secs|
54
+ secs = secs.to_i
55
+ return '0h' if secs <= 0
56
+
57
+ minutes = (secs / 60).to_i
58
+ hours = (minutes / 60).to_i
59
+ mins = (minutes % 60).to_i
60
+ return format('%<h>dh', h: hours) if mins.zero?
61
+ return format('%<m>dm', m: mins) if hours.zero?
62
+
63
+ format('%<h>dh%<m>dm', h: hours, m: mins)
64
+ end
65
+
66
+ budgets.each do |tag, budget_secs|
67
+ used = tag_totals[tag].to_i
68
+ remaining = budget_secs.to_i - used
69
+ remaining = 0 if remaining.negative?
70
+ budgets_total += remaining
71
+ end
72
+
40
73
  width = wwid.config['plugins']['byday']['item_width'].to_i || 60
41
74
  divider = "{wd}+{xk}#{'-' * 10}{wd}+{xk}#{'-' * width}{wd}+{xk}#{'-' * 8}{wd}+{x}"
42
75
  out = []
@@ -54,11 +87,17 @@ module Doing
54
87
  out << "{wd}| |{xbw}#{title}{wd}|{xy}#{interval}{wd}|{x}"
55
88
  end
56
89
  day_total = "Total: #{totals[day].time_string(format: :clock)}"
90
+ if budgets_total.positive?
91
+ day_total += " (total budgets left #{budget_fmt.call(budgets_total)})"
92
+ end
57
93
  out << divider
58
94
  out << "{wd}|{xg}#{day_total.rjust(width + 20)}{wd}|{x}"
59
95
  out << divider
60
96
  end
61
97
  all_total = "Grand Total: #{total.time_string(format: :clock)}"
98
+ if budgets_total.positive?
99
+ all_total += " (total budgets left #{budget_fmt.call(budgets_total)})"
100
+ end
62
101
  out << "{wd}|{xrb}#{all_total.rjust(width + 20)}{wd}|{x}"
63
102
  out << divider
64
103
  Doing::Color.template(out.join("\n"))
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.93'
4
+ VERSION = '2.1.97'
5
5
  end
@@ -16,6 +16,34 @@ module Doing
16
16
 
17
17
  @timers.delete('meanwhile')
18
18
 
19
+ timers_snapshot = @timers.dup
20
+
21
+ budgets = Doing.setting('budgets', {}) || {}
22
+ budgets = budgets.transform_keys { |k| k.to_s.downcase }
23
+ remaining_map = {}
24
+ budgets_total = 0
25
+
26
+ budget_fmt = lambda do |secs|
27
+ secs = secs.to_i
28
+ return '0h' if secs <= 0
29
+
30
+ minutes = (secs / 60).to_i
31
+ hours = (minutes / 60).to_i
32
+ mins = (minutes % 60).to_i
33
+ return format('%dh', hours) if mins.zero?
34
+ return format('%dm', mins) if hours.zero?
35
+
36
+ format('%dh%dm', hours, mins)
37
+ end
38
+
39
+ budgets.each do |tag, budget_secs|
40
+ used = timers_snapshot[tag].to_i
41
+ remaining = budget_secs.to_i - used
42
+ remaining = 0 if remaining.negative?
43
+ remaining_map[tag] = remaining
44
+ budgets_total += remaining
45
+ end
46
+
19
47
  max = @timers.keys.sort_by(&:length).reverse[0].length + 1
20
48
 
21
49
  total = @timers.delete('All')
@@ -47,9 +75,13 @@ module Doing
47
75
  <tbody>
48
76
  EOHEAD
49
77
  sorted_tags_data.reverse.each do |k, v|
50
- if v.positive?
51
- output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}</td></tr>\n"
78
+ next unless v.positive?
79
+
80
+ budget_str = ''
81
+ if remaining_map.key?(k) && remaining_map[k].positive?
82
+ budget_str = " (budget left #{budget_fmt.call(remaining_map[k])})"
52
83
  end
84
+ output += "<tr><td style='text-align:left;'>#{k}</td><td style='text-align:left;'>#{v.time_string(format: :clock)}#{budget_str}</td></tr>\n"
53
85
  end
54
86
  tail = <<EOTAIL
55
87
  <tr>
@@ -59,7 +91,7 @@ EOHEAD
59
91
  <tfoot>
60
92
  <tr>
61
93
  <td style="text-align:left;"><strong>Total</strong></td>
62
- <td style="text-align:left;">#{total.time_string(format: :clock)}</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>
63
95
  </tr>
64
96
  </tfoot>
65
97
  </table>
@@ -73,7 +105,13 @@ EOTAIL
73
105
  | #{'-' * (pad - 1)}: | :------- |
74
106
  EOHEADER
75
107
  sorted_tags_data.reverse.each do |k, v|
76
- output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)} |\n" if v.positive?
108
+ next unless v.positive?
109
+
110
+ budget_str = ''
111
+ if remaining_map.key?(k) && remaining_map[k].positive?
112
+ budget_str = " (budget left #{budget_fmt.call(remaining_map[k])})"
113
+ end
114
+ output += "| #{' ' * (pad - k.length)}#{k} | #{v.time_string(format: :clock)}#{budget_str} |\n"
77
115
  end
78
116
  tail = '[Tag Totals]'
79
117
  output + tail
@@ -83,7 +121,10 @@ EOTAIL
83
121
  output << {
84
122
  'tag' => k,
85
123
  'seconds' => v,
86
- 'formatted' => v.time_string(format: :clock)
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)
87
128
  }
88
129
  end
89
130
  output
@@ -94,17 +135,33 @@ EOTAIL
94
135
  (max - k.length).times do
95
136
  spacer += ' '
96
137
  end
97
- output.push("┃ #{spacer}#{k}:#{v.time_string(format: :hm)}")
138
+ line = "┃ #{spacer}#{k}:#{v.time_string(format: :hm)}"
139
+ if remaining_map.key?(k) && remaining_map[k].positive?
140
+ line += " (budget left #{budget_fmt.call(remaining_map[k])})"
141
+ end
142
+ line += ' ┃'
143
+ output.push(line)
144
+ end
145
+
146
+ 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?
148
+ total_content += ' ┃'
149
+ max_line_len = (output + [total_content]).map(&:length).max
150
+
151
+ pad_line = lambda do |line|
152
+ pad = max_line_len - line.length
153
+ pad.positive? ? "#{line[0..-3]} #{' ' * pad}┃" : line
98
154
  end
155
+ output = output.map { |l| pad_line.call(l) }
99
156
 
100
157
  header = '┏━━ Tag Totals '
101
- (max - 2).times { header += '━' }
158
+ [(max_line_len - 16), 0].max.times { header += '━' }
102
159
  header += '┓'
103
160
  footer = '┗'
104
- (max + 12).times { footer += '━' }
161
+ [(max_line_len - 2), 0].max.times { footer += '━' }
105
162
  footer += '┛'
106
163
  divider = '┣'
107
- (max + 12).times { divider += '━' }
164
+ [(max_line_len - 2), 0].max.times { divider += '━' }
108
165
  divider += '┫'
109
166
  output = output.empty? ? '' : "\n#{header}\n#{output.join("\n")}"
110
167
  output += "\n#{divider}"
@@ -113,10 +170,12 @@ EOTAIL
113
170
  spacer += ' '
114
171
  end
115
172
  total_time = total.time_string(format: :hm)
116
- total = "┃ #{spacer}total: "
117
- total += total_time
118
- total += ' ┃'
119
- output += "\n#{total}"
173
+ total_line = "┃ #{spacer}total: "
174
+ total_line += total_time
175
+ total_line += " (total budgets left #{budget_fmt.call(budgets_total)})" if budgets_total.positive?
176
+ total_line += ' ┃'
177
+ total_line = pad_line.call(total_line)
178
+ output += "\n#{total_line}"
120
179
  output += "\n#{footer}"
121
180
  output
122
181
  else
@@ -126,11 +185,19 @@ EOTAIL
126
185
  (max - k.length).times do
127
186
  spacer += ' '
128
187
  end
129
- output.push("#{k}:#{spacer}#{v.time_string(format: :clock)}")
188
+ line = "#{k}:#{spacer}#{v.time_string(format: :clock)}"
189
+ if remaining_map.key?(k) && remaining_map[k].positive?
190
+ line += " (budget left #{budget_fmt.call(remaining_map[k])})"
191
+ end
192
+ output.push(line)
130
193
  end
131
194
 
132
195
  output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
133
- output += "\n\nTotal tracked: #{total.time_string(format: :clock)}\n"
196
+ 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
200
+ output += "\n"
134
201
  output
135
202
  end
136
203
  end
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.93
4
+ version: 2.1.97
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -514,6 +514,7 @@ files:
514
514
  - _config.yml
515
515
  - bin/commands/again.rb
516
516
  - bin/commands/archive.rb
517
+ - bin/commands/budget.rb
517
518
  - bin/commands/cancel.rb
518
519
  - bin/commands/changes.rb
519
520
  - bin/commands/choose.rb