doing 2.1.91 → 2.1.94

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -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 +3 -3
  8. data/docs/doc/BooleanTermParser/Clause.html +3 -3
  9. data/docs/doc/BooleanTermParser/Operator.html +3 -3
  10. data/docs/doc/BooleanTermParser/Query.html +3 -3
  11. data/docs/doc/BooleanTermParser/QueryParser.html +3 -3
  12. data/docs/doc/BooleanTermParser/QueryTransformer.html +3 -3
  13. data/docs/doc/BooleanTermParser.html +3 -3
  14. data/docs/doc/Doing/ArrayCleanup.html +3 -3
  15. data/docs/doc/Doing/ArrayNestedHash.html +3 -3
  16. data/docs/doc/Doing/ArrayTags.html +3 -3
  17. data/docs/doc/Doing/ByDayExport.html +3 -3
  18. data/docs/doc/Doing/CSVExport.html +3 -3
  19. data/docs/doc/Doing/CalendarImport.html +3 -3
  20. data/docs/doc/Doing/Change.html +3 -3
  21. data/docs/doc/Doing/Changes.html +3 -3
  22. data/docs/doc/Doing/ChronifyArray.html +3 -3
  23. data/docs/doc/Doing/ChronifyNumeric.html +3 -3
  24. data/docs/doc/Doing/ChronifyString.html +3 -3
  25. data/docs/doc/Doing/Color.html +3 -3
  26. data/docs/doc/Doing/Completion/BashCompletions.html +3 -3
  27. data/docs/doc/Doing/Completion/FigCompletions.html +3 -3
  28. data/docs/doc/Doing/Completion/FishCompletions.html +3 -3
  29. data/docs/doc/Doing/Completion/StringUtils.html +3 -3
  30. data/docs/doc/Doing/Completion/ZshCompletions.html +3 -3
  31. data/docs/doc/Doing/Completion.html +3 -3
  32. data/docs/doc/Doing/Configuration.html +5 -3
  33. data/docs/doc/Doing/DayOneRenderer.html +3 -3
  34. data/docs/doc/Doing/DayoneExport.html +3 -3
  35. data/docs/doc/Doing/DoingExport.html +3 -3
  36. data/docs/doc/Doing/DoingImport.html +3 -3
  37. data/docs/doc/Doing/Entry.html +3 -3
  38. data/docs/doc/Doing/Errors/DoingNoTraceError.html +3 -3
  39. data/docs/doc/Doing/Errors/DoingRuntimeError.html +3 -3
  40. data/docs/doc/Doing/Errors/DoingStandardError.html +3 -3
  41. data/docs/doc/Doing/Errors/EmptyInput.html +3 -3
  42. data/docs/doc/Doing/Errors/HistoryLimitError.html +3 -3
  43. data/docs/doc/Doing/Errors/InvalidPlugin.html +3 -3
  44. data/docs/doc/Doing/Errors/MissingBackupFile.html +3 -3
  45. data/docs/doc/Doing/Errors/NoResults.html +3 -3
  46. data/docs/doc/Doing/Errors/PluginException.html +3 -3
  47. data/docs/doc/Doing/Errors/UserCancelled.html +3 -3
  48. data/docs/doc/Doing/Errors/WrongCommand.html +3 -3
  49. data/docs/doc/Doing/Errors.html +3 -3
  50. data/docs/doc/Doing/HTMLExport.html +3 -3
  51. data/docs/doc/Doing/Hooks.html +3 -3
  52. data/docs/doc/Doing/Item.html +3 -3
  53. data/docs/doc/Doing/ItemDates.html +3 -3
  54. data/docs/doc/Doing/ItemQuery.html +3 -3
  55. data/docs/doc/Doing/ItemState.html +3 -3
  56. data/docs/doc/Doing/ItemTags.html +3 -3
  57. data/docs/doc/Doing/Items.html +3 -3
  58. data/docs/doc/Doing/JSONExport.html +3 -3
  59. data/docs/doc/Doing/JSONImport.html +3 -3
  60. data/docs/doc/Doing/Logger.html +3 -3
  61. data/docs/doc/Doing/MarkdownExport.html +3 -3
  62. data/docs/doc/Doing/Note.html +3 -3
  63. data/docs/doc/Doing/Pager.html +3 -3
  64. data/docs/doc/Doing/Plugins.html +3 -3
  65. data/docs/doc/Doing/Prompt.html +3 -3
  66. data/docs/doc/Doing/PromptChoose.html +3 -3
  67. data/docs/doc/Doing/PromptFZF.html +3 -3
  68. data/docs/doc/Doing/PromptInput.html +3 -3
  69. data/docs/doc/Doing/PromptSTD.html +3 -3
  70. data/docs/doc/Doing/PromptYN.html +3 -3
  71. data/docs/doc/Doing/Section.html +3 -3
  72. data/docs/doc/Doing/StringHighlight.html +3 -3
  73. data/docs/doc/Doing/StringNormalize.html +3 -3
  74. data/docs/doc/Doing/StringQuery.html +3 -3
  75. data/docs/doc/Doing/StringTags.html +3 -3
  76. data/docs/doc/Doing/StringTransform.html +3 -3
  77. data/docs/doc/Doing/StringTruncate.html +3 -3
  78. data/docs/doc/Doing/StringURL.html +3 -3
  79. data/docs/doc/Doing/SymbolNormalize.html +3 -3
  80. data/docs/doc/Doing/TaskPaperExport.html +3 -3
  81. data/docs/doc/Doing/TemplateExport.html +3 -3
  82. data/docs/doc/Doing/TemplateString.html +3 -3
  83. data/docs/doc/Doing/TimingImport.html +3 -3
  84. data/docs/doc/Doing/Types.html +3 -3
  85. data/docs/doc/Doing/Util/Backup.html +3 -3
  86. data/docs/doc/Doing/Util.html +3 -3
  87. data/docs/doc/Doing/Version.html +3 -3
  88. data/docs/doc/Doing/WWID.html +3 -3
  89. data/docs/doc/Doing.html +4 -4
  90. data/docs/doc/FalseClass.html +3 -3
  91. data/docs/doc/GLI/Commands/Help.html +3 -3
  92. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +3 -3
  93. data/docs/doc/GLI/Commands.html +3 -3
  94. data/docs/doc/GLI.html +3 -3
  95. data/docs/doc/Hash.html +3 -3
  96. data/docs/doc/Numeric.html +3 -3
  97. data/docs/doc/Object.html +3 -3
  98. data/docs/doc/PhraseParser/Operator.html +3 -3
  99. data/docs/doc/PhraseParser/PhraseClause.html +3 -3
  100. data/docs/doc/PhraseParser/Query.html +3 -3
  101. data/docs/doc/PhraseParser/QueryParser.html +3 -3
  102. data/docs/doc/PhraseParser/QueryTransformer.html +3 -3
  103. data/docs/doc/PhraseParser/TermClause.html +3 -3
  104. data/docs/doc/PhraseParser.html +3 -3
  105. data/docs/doc/Status.html +3 -3
  106. data/docs/doc/String.html +3 -3
  107. data/docs/doc/Symbol.html +3 -3
  108. data/docs/doc/Time.html +3 -3
  109. data/docs/doc/TrueClass.html +3 -3
  110. data/docs/doc/_index.html +4 -4
  111. data/docs/doc/css/style.css +2 -15
  112. data/docs/doc/file.README.html +4 -4
  113. data/docs/doc/frames.html +1 -1
  114. data/docs/doc/index.html +4 -4
  115. data/docs/doc/js/app.js +91 -40
  116. data/docs/doc/js/full_list.js +22 -20
  117. data/docs/doc/top-level-namespace.html +3 -3
  118. data/doing.rdoc +17 -1
  119. data/lib/completion/_doing.zsh +4 -0
  120. data/lib/completion/doing.bash +11 -0
  121. data/lib/completion/doing.fish +2 -0
  122. data/lib/completion/doing.ts +14 -0
  123. data/lib/doing/configuration.rb +4 -2
  124. data/lib/doing/plugins/export/byday.rb +39 -0
  125. data/lib/doing/version.rb +1 -1
  126. data/lib/doing/wwid/modify.rb +2 -1
  127. data/lib/doing/wwid/timers.rb +65 -8
  128. metadata +3 -2
data/docs/doc/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- (function () {
1
+ window.__app = function () {
2
2
  var localStorage = {},
3
3
  sessionStorage = {};
4
4
  try {
@@ -153,7 +153,6 @@
153
153
  });
154
154
  // Add the value of the constant as "Tooltip" to the summary object
155
155
  list.find("pre.code").each(function () {
156
- console.log($(this).parent());
157
156
  var dt_element = $(this).parent().prev();
158
157
  var tooltip = $(this).text();
159
158
  if (dt_element.hasClass("deprecated")) {
@@ -250,37 +249,46 @@
250
249
  );
251
250
  }
252
251
 
253
- function navResizeFn(e) {
254
- if (e.which !== 1) {
255
- navResizeFnStop();
256
- return;
257
- }
258
-
259
- sessionStorage.navWidth = e.pageX.toString();
260
- $(".nav_wrap").css("width", e.pageX);
261
- $(".nav_wrap").css("-ms-flex", "inherit");
262
- }
263
-
264
- function navResizeFnStop() {
265
- $(window).unbind("mousemove", navResizeFn);
266
- window.removeEventListener("message", navMessageFn, false);
267
- }
268
-
269
- function navMessageFn(e) {
270
- if (e.data.action === "mousemove") navResizeFn(e.data.event);
271
- if (e.data.action === "mouseup") navResizeFnStop();
272
- }
273
-
274
252
  function navResizer() {
275
- $("#resizer").mousedown(function (e) {
276
- e.preventDefault();
277
- $(window).mousemove(navResizeFn);
278
- window.addEventListener("message", navMessageFn, false);
279
- });
280
- $(window).mouseup(navResizeFnStop);
253
+ const resizer = document.getElementById("resizer");
254
+ resizer.addEventListener(
255
+ "pointerdown",
256
+ function (e) {
257
+ resizer.setPointerCapture(e.pointerId);
258
+ e.preventDefault();
259
+ e.stopPropagation();
260
+ },
261
+ false
262
+ );
263
+ resizer.addEventListener(
264
+ "pointerup",
265
+ function (e) {
266
+ resizer.releasePointerCapture(e.pointerId);
267
+ e.preventDefault();
268
+ e.stopPropagation();
269
+ },
270
+ false
271
+ );
272
+ resizer.addEventListener(
273
+ "pointermove",
274
+ function (e) {
275
+ if ((e.buttons & 1) === 0) {
276
+ return;
277
+ }
278
+
279
+ sessionStorage.navWidth = e.pageX.toString();
280
+ $(".nav_wrap").css("width", Math.max(200, e.pageX));
281
+ e.preventDefault();
282
+ e.stopPropagation();
283
+ },
284
+ false
285
+ );
281
286
 
282
287
  if (sessionStorage.navWidth) {
283
- navResizeFn({ which: 1, pageX: parseInt(sessionStorage.navWidth, 10) });
288
+ $(".nav_wrap").css(
289
+ "width",
290
+ Math.max(200, parseInt(sessionStorage.navWidth, 10))
291
+ );
284
292
  }
285
293
  }
286
294
 
@@ -295,15 +303,6 @@
295
303
  document.getElementById("nav").contentWindow.postMessage(opts, "*");
296
304
  done = true;
297
305
  }
298
-
299
- window.addEventListener(
300
- "message",
301
- function (event) {
302
- if (event.data === "navReady") postMessage();
303
- return false;
304
- },
305
- false
306
- );
307
306
  }
308
307
 
309
308
  function mainFocus() {
@@ -341,4 +340,56 @@
341
340
  mainFocus();
342
341
  navigationChange();
343
342
  });
344
- })();
343
+ };
344
+ window.__app();
345
+
346
+ window.addEventListener(
347
+ "message",
348
+ async (e) => {
349
+ if (e.data.action === "navigate") {
350
+ const response = await fetch(e.data.url);
351
+ const text = await response.text();
352
+ const parser = new DOMParser();
353
+ const doc = parser.parseFromString(text, "text/html");
354
+
355
+ const classListLink =
356
+ document.getElementById("class_list_link").classList;
357
+
358
+ const content = doc.querySelector("#main").innerHTML;
359
+ document.querySelector("#main").innerHTML = content;
360
+ document.title = doc.head.querySelector("title").innerText;
361
+ document.head.querySelectorAll("script").forEach((script) => {
362
+ if (
363
+ !script.type ||
364
+ (script.type.includes("text/javascript") && !script.src)
365
+ ) {
366
+ script.remove();
367
+ }
368
+ });
369
+
370
+ doc.head.querySelectorAll("script").forEach((script) => {
371
+ if (
372
+ !script.type ||
373
+ (script.type.includes("text/javascript") && !script.src)
374
+ ) {
375
+ const newScript = document.createElement("script");
376
+ newScript.type = "text/javascript";
377
+ newScript.textContent = script.textContent;
378
+ document.head.appendChild(newScript);
379
+ }
380
+ });
381
+
382
+ window.__app();
383
+
384
+ document.getElementById("class_list_link").classList = classListLink;
385
+
386
+ const url = new URL(e.data.url, "https://localhost");
387
+ const hash = decodeURIComponent(url.hash ?? "");
388
+ if (hash) {
389
+ document.getElementById(hash.substring(1)).scrollIntoView();
390
+ }
391
+ history.pushState({}, document.title, e.data.url);
392
+ }
393
+ },
394
+ false
395
+ );
@@ -20,17 +20,6 @@ function escapeShortcut() {
20
20
  });
21
21
  }
22
22
 
23
- function navResizer() {
24
- $(window).mousemove(function(e) {
25
- window.parent.postMessage({
26
- action: 'mousemove', event: {pageX: e.pageX, which: e.which}
27
- }, '*');
28
- }).mouseup(function(e) {
29
- window.parent.postMessage({action: 'mouseup'}, '*');
30
- });
31
- window.parent.postMessage("navReady", "*");
32
- }
33
-
34
23
  function clearSearchTimeout() {
35
24
  clearTimeout(searchTimeout);
36
25
  searchTimeout = null;
@@ -44,14 +33,21 @@ function enableLinks() {
44
33
  $clicked.addClass('clicked');
45
34
  evt.stopPropagation();
46
35
 
47
- if (evt.target.tagName === 'A') return true;
36
+ if (window.origin === "null") {
37
+ if (evt.target.tagName === 'A') return true;
48
38
 
49
- var elem = $clicked.find('> .item .object_link a')[0];
50
- var e = evt.originalEvent;
51
- var newEvent = new MouseEvent(evt.originalEvent.type);
52
- newEvent.initMouseEvent(e.type, e.canBubble, e.cancelable, e.view, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
53
- elem.dispatchEvent(newEvent);
54
- evt.preventDefault();
39
+ var elem = $clicked.find('> .item .object_link a')[0];
40
+ var e = evt.originalEvent;
41
+ var newEvent = new MouseEvent(evt.originalEvent.type);
42
+ newEvent.initMouseEvent(e.type, e.canBubble, e.cancelable, e.view, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
43
+ elem.dispatchEvent(newEvent);
44
+ evt.preventDefault();
45
+ } else {
46
+ window.top.postMessage({
47
+ action: "navigate",
48
+ url: $clicked.find('.object_link a').attr('href'),
49
+ }, "*");
50
+ }
55
51
  return false;
56
52
  });
57
53
  }
@@ -199,6 +195,13 @@ function highlight() {
199
195
  });
200
196
  }
201
197
 
198
+ function isInView(element) {
199
+ const rect = element.getBoundingClientRect();
200
+ const windowHeight =
201
+ window.innerHeight || document.documentElement.clientHeight;
202
+ return rect.left >= 0 && rect.bottom <= windowHeight;
203
+ }
204
+
202
205
  /**
203
206
  * Expands the tree to the target element and its immediate
204
207
  * children.
@@ -214,7 +217,7 @@ function expandTo(path) {
214
217
  $(el).find('> div > a.toggle').attr('aria-expanded', 'true');
215
218
  });
216
219
 
217
- if($target[0]) {
220
+ if($target[0] && !isInView($target[0])) {
218
221
  window.scrollTo(window.scrollX, $target.offset().top - 250);
219
222
  highlight();
220
223
  }
@@ -232,7 +235,6 @@ window.addEventListener("message", windowEvents, false);
232
235
 
233
236
  $(document).ready(function() {
234
237
  escapeShortcut();
235
- navResizer();
236
238
  enableLinks();
237
239
  enableToggles();
238
240
  populateSearchCache();
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  Top Level Namespace
8
8
 
9
- &mdash; Documentation by YARD 0.9.37
9
+ &mdash; Documentation by YARD 0.9.38
10
10
 
11
11
  </title>
12
12
 
@@ -216,9 +216,9 @@
216
216
  </div>
217
217
 
218
218
  <div id="footer">
219
- Generated on Fri Dec 5 16:12:00 2025 by
219
+ Generated on Fri Feb 13 07:19:08 2026 by
220
220
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
221
- 0.9.37 (ruby-3.4.4).
221
+ 0.9.38 (ruby-3.4.4).
222
222
  </div>
223
223
 
224
224
  </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.91
8
+ v2.1.94
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.91'
4
+ VERSION = '2.1.94'
5
5
  end
@@ -186,7 +186,8 @@ module Doing
186
186
  items = filter_items(Items.new, opt: opt)
187
187
 
188
188
  if opt[:interactive]
189
- items = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, menu: true,
189
+ include_section = Array(opt[:section]).any? { |sec| sec.to_s =~ /^all$/i }
190
+ items = Prompt.choose_from_items(items, include_section: include_section, menu: true,
190
191
  header: '',
191
192
  prompt: 'Select entries to tag > ',
192
193
  multiple: true,
@@ -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,7 +135,12 @@ 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)
98
144
  end
99
145
 
100
146
  header = '┏━━ Tag Totals '
@@ -115,6 +161,9 @@ EOTAIL
115
161
  total_time = total.time_string(format: :hm)
116
162
  total = "┃ #{spacer}total: "
117
163
  total += total_time
164
+ if budgets_total.positive?
165
+ total += " (total budgets left #{budget_fmt.call(budgets_total)})"
166
+ end
118
167
  total += ' ┃'
119
168
  output += "\n#{total}"
120
169
  output += "\n#{footer}"
@@ -126,11 +175,19 @@ EOTAIL
126
175
  (max - k.length).times do
127
176
  spacer += ' '
128
177
  end
129
- output.push("#{k}:#{spacer}#{v.time_string(format: :clock)}")
178
+ line = "#{k}:#{spacer}#{v.time_string(format: :clock)}"
179
+ if remaining_map.key?(k) && remaining_map[k].positive?
180
+ line += " (budget left #{budget_fmt.call(remaining_map[k])})"
181
+ end
182
+ output.push(line)
130
183
  end
131
184
 
132
185
  output = output.empty? ? '' : "\n--- Tag Totals ---\n#{output.join("\n")}"
133
- output += "\n\nTotal tracked: #{total.time_string(format: :clock)}\n"
186
+ output += "\n\nTotal tracked: #{total.time_string(format: :clock)}"
187
+ if budgets_total.positive?
188
+ output += " (total budgets left #{budget_fmt.call(budgets_total)})"
189
+ end
190
+ output += "\n"
134
191
  output
135
192
  end
136
193
  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.91
4
+ version: 2.1.94
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
@@ -943,7 +944,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
943
944
  - !ruby/object:Gem::Version
944
945
  version: '0'
945
946
  requirements: []
946
- rubygems_version: 3.6.7
947
+ rubygems_version: 4.0.3
947
948
  specification_version: 4
948
949
  summary: A command line tool for managing What Was I Doing reminders
949
950
  test_files: []