doing 2.1.111 → 2.1.115

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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/docs/doc/Array.html +1 -1
  6. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  7. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  8. data/docs/doc/BooleanTermParser/Query.html +1 -1
  9. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  10. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  11. data/docs/doc/BooleanTermParser.html +1 -1
  12. data/docs/doc/Doing/ArrayCleanup.html +1 -1
  13. data/docs/doc/Doing/ArrayNestedHash.html +1 -1
  14. data/docs/doc/Doing/ArrayTags.html +1 -1
  15. data/docs/doc/Doing/ByDayExport.html +1 -1
  16. data/docs/doc/Doing/CSVExport.html +1 -1
  17. data/docs/doc/Doing/CalendarImport.html +1 -1
  18. data/docs/doc/Doing/Change.html +1 -1
  19. data/docs/doc/Doing/Changes.html +1 -1
  20. data/docs/doc/Doing/ChronifyArray.html +1 -1
  21. data/docs/doc/Doing/ChronifyNumeric.html +1 -1
  22. data/docs/doc/Doing/ChronifyString.html +1 -1
  23. data/docs/doc/Doing/Color.html +1 -1
  24. data/docs/doc/Doing/Completion/BashCompletions.html +1 -1
  25. data/docs/doc/Doing/Completion/FigCompletions.html +1 -1
  26. data/docs/doc/Doing/Completion/FishCompletions.html +1 -1
  27. data/docs/doc/Doing/Completion/StringUtils.html +1 -1
  28. data/docs/doc/Doing/Completion/ZshCompletions.html +1 -1
  29. data/docs/doc/Doing/Completion.html +1 -1
  30. data/docs/doc/Doing/Configuration.html +1 -1
  31. data/docs/doc/Doing/DayOneRenderer.html +1 -1
  32. data/docs/doc/Doing/DayoneExport.html +1 -1
  33. data/docs/doc/Doing/DoingExport.html +1 -1
  34. data/docs/doc/Doing/DoingImport.html +1 -1
  35. data/docs/doc/Doing/Entry.html +1 -1
  36. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  37. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  38. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  39. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  40. data/docs/doc/Doing/Errors/HistoryLimitError.html +1 -1
  41. data/docs/doc/Doing/Errors/InvalidPlugin.html +1 -1
  42. data/docs/doc/Doing/Errors/MissingBackupFile.html +1 -1
  43. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  44. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  45. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  46. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  47. data/docs/doc/Doing/Errors.html +1 -1
  48. data/docs/doc/Doing/HTMLExport.html +1 -1
  49. data/docs/doc/Doing/Hooks.html +1 -1
  50. data/docs/doc/Doing/Item.html +1 -1
  51. data/docs/doc/Doing/ItemDates.html +1 -1
  52. data/docs/doc/Doing/ItemQuery.html +1 -1
  53. data/docs/doc/Doing/ItemState.html +1 -1
  54. data/docs/doc/Doing/ItemTags.html +1 -1
  55. data/docs/doc/Doing/Items.html +1 -1
  56. data/docs/doc/Doing/JSONExport.html +1 -1
  57. data/docs/doc/Doing/JSONImport.html +1 -1
  58. data/docs/doc/Doing/Logger.html +1 -1
  59. data/docs/doc/Doing/MarkdownExport.html +1 -1
  60. data/docs/doc/Doing/Note.html +1 -1
  61. data/docs/doc/Doing/Pager.html +1 -1
  62. data/docs/doc/Doing/Plugins.html +1 -1
  63. data/docs/doc/Doing/Prompt.html +1 -1
  64. data/docs/doc/Doing/PromptChoose.html +1 -1
  65. data/docs/doc/Doing/PromptFZF.html +1 -1
  66. data/docs/doc/Doing/PromptInput.html +1 -1
  67. data/docs/doc/Doing/PromptSTD.html +1 -1
  68. data/docs/doc/Doing/PromptYN.html +1 -1
  69. data/docs/doc/Doing/Section.html +1 -1
  70. data/docs/doc/Doing/StringHighlight.html +35 -1
  71. data/docs/doc/Doing/StringNormalize.html +1 -1
  72. data/docs/doc/Doing/StringQuery.html +1 -1
  73. data/docs/doc/Doing/StringTags.html +1 -1
  74. data/docs/doc/Doing/StringTransform.html +1 -1
  75. data/docs/doc/Doing/StringTruncate.html +1 -1
  76. data/docs/doc/Doing/StringURL.html +1 -1
  77. data/docs/doc/Doing/SymbolNormalize.html +1 -1
  78. data/docs/doc/Doing/TaskPaperExport.html +1 -1
  79. data/docs/doc/Doing/TemplateExport.html +1 -1
  80. data/docs/doc/Doing/TemplateString.html +2 -2
  81. data/docs/doc/Doing/TimingImport.html +1 -1
  82. data/docs/doc/Doing/Types.html +1 -1
  83. data/docs/doc/Doing/Util/Backup.html +1 -1
  84. data/docs/doc/Doing/Util.html +1 -1
  85. data/docs/doc/Doing/Version.html +1 -1
  86. data/docs/doc/Doing/WWID.html +18 -2
  87. data/docs/doc/Doing.html +2 -2
  88. data/docs/doc/FalseClass.html +1 -1
  89. data/docs/doc/GLI/Commands/Help.html +1 -1
  90. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  91. data/docs/doc/GLI/Commands.html +1 -1
  92. data/docs/doc/GLI.html +1 -1
  93. data/docs/doc/Hash.html +1 -1
  94. data/docs/doc/Numeric.html +1 -1
  95. data/docs/doc/Object.html +1 -1
  96. data/docs/doc/PhraseParser/Operator.html +1 -1
  97. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  98. data/docs/doc/PhraseParser/Query.html +1 -1
  99. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  100. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  101. data/docs/doc/PhraseParser/TermClause.html +1 -1
  102. data/docs/doc/PhraseParser.html +1 -1
  103. data/docs/doc/Status.html +1 -1
  104. data/docs/doc/String.html +1 -1
  105. data/docs/doc/Symbol.html +1 -1
  106. data/docs/doc/Time.html +1 -1
  107. data/docs/doc/TrueClass.html +1 -1
  108. data/docs/doc/_index.html +1 -1
  109. data/docs/doc/file.README.html +2 -2
  110. data/docs/doc/index.html +2 -2
  111. data/docs/doc/method_list.html +548 -540
  112. data/docs/doc/top-level-namespace.html +1 -1
  113. data/doing.rdoc +1 -1
  114. data/lib/doing/plugins/export/template_export.rb +1 -1
  115. data/lib/doing/string/highlight.rb +9 -3
  116. data/lib/doing/string/transform.rb +1 -1
  117. data/lib/doing/template_string.rb +147 -6
  118. data/lib/doing/version.rb +1 -1
  119. data/lib/doing/wwid/display.rb +45 -2
  120. data/man/doing.1.ronn +2 -0
  121. metadata +1 -1
@@ -216,7 +216,7 @@
216
216
  </div>
217
217
 
218
218
  <div id="footer">
219
- Generated on Sat Apr 25 02:14:07 2026 by
219
+ Generated on Wed Apr 29 04:23:14 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.111
8
+ v2.1.115
9
9
 
10
10
  === Global Options
11
11
  === --config_file arg
@@ -103,7 +103,7 @@ module Doing
103
103
  template = opt[:template].dup
104
104
  note_rx = /(?i-m)(?x:^([\s\S]*?)
105
105
  (%(?:[io]d|(?:\^[\s\S])?
106
- (?:(?:[ _t]|[^a-z0-9])?\d+)?
106
+ (?:(?:[ _t]|[^a-z0-9])?(?:\*|\d+))?
107
107
  (?:[\s\S][ _t]?)?)?note)
108
108
  ([\s\S]*?)$)/
109
109
  template.sub!(note_rx, '\1\3\2')
@@ -5,7 +5,7 @@ module Doing
5
5
  module StringHighlight
6
6
  ## @param (see #highlight_tags)
7
7
  def highlight_tags!(color = 'yellow', last_color: nil)
8
- replace highlight_tags(color)
8
+ replace highlight_tags(color, last_color: last_color)
9
9
  end
10
10
 
11
11
  ##
@@ -71,7 +71,13 @@ module Doing
71
71
  # @return [String] All escape codes in string
72
72
  #
73
73
  def last_color
74
- scan(/\e\[[\d;]+m/).join('')
74
+ scan(Doing::Color::ESCAPE_REGEX).join('')
75
+ end
76
+
77
+ def last_color_code
78
+ return '' unless match?(Doing::Color::ESCAPE_REGEX)
79
+
80
+ super
75
81
  end
76
82
 
77
83
  ##
@@ -80,7 +86,7 @@ module Doing
80
86
  ## @return clean string
81
87
  ##
82
88
  def uncolor
83
- gsub(/\e\[[\d;]+m/, '')
89
+ gsub(Doing::Color::ESCAPE_REGEX, '')
84
90
  end
85
91
 
86
92
  ##
@@ -46,7 +46,7 @@ module Doing
46
46
  ##
47
47
  def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '', pad_first: false)
48
48
  color.empty? ? '' : after.last_color
49
- note_rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?note/
49
+ note_rx = /(?mi)(?<!\\)%(?<width>\*|-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?note/
50
50
  note = ''
51
51
  after = after.dup if after.frozen?
52
52
  after.sub!(note_rx) do
@@ -15,6 +15,7 @@ module Doing
15
15
  @original = string
16
16
  super(Doing::Color.coloring? ? Color.reset + string : string)
17
17
 
18
+ @stretch_widths = stretch_widths(placeholders)
18
19
  placeholders.each { |k, v| fill(k, v, wrap_width: wrap_width, color: color, tags_color: tags_color) }
19
20
  end
20
21
 
@@ -97,9 +98,8 @@ module Doing
97
98
 
98
99
  def fill(placeholder, value, wrap_width: 0, color: '', tags_color: '', reset: '')
99
100
  reparse
100
- rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?#{placeholder.sub(
101
- /^%/, ''
102
- )}(?<after>.*?)$/
101
+ placeholder_name = placeholder.sub(/^%/, '')
102
+ rx = /(?mi)(?<!\\)%(?<width>\*|-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?#{placeholder_name}(?<after>.*?)$/
103
103
  ph = raw.match(rx)
104
104
 
105
105
  return unless ph
@@ -117,9 +117,9 @@ module Doing
117
117
  if !value.good?
118
118
  after
119
119
  else
120
- pad = m['width'].to_i
120
+ pad = m['width'] == '*' ? next_stretch_width(placeholder_name) : m['width'].to_i
121
121
  mark = m['mchar'] || ''
122
- if placeholder == 'shortdate' && m['width'].nil?
122
+ if placeholder_name == 'shortdate' && m['width'].nil?
123
123
  fmt_string = Doing.setting('shortdate_format.older', '%m/%d/%y %_I:%M%P', exact: true)
124
124
  pad = Date.today.strftime(fmt_string).length
125
125
  end
@@ -142,7 +142,6 @@ module Doing
142
142
 
143
143
  if wrap_width.positive? || pad.positive?
144
144
  width = pad.positive? ? pad : wrap_width
145
-
146
145
  out = value.gsub(/%/, '\%').strip.wrap(width,
147
146
  pad: pad,
148
147
  indent: indent,
@@ -184,5 +183,147 @@ module Doing
184
183
  end
185
184
  @parsed_colors = parse_colors
186
185
  end
186
+
187
+ private
188
+
189
+ def stretch_widths(placeholders)
190
+ keys = placeholders.keys.map { |k| Regexp.escape(k.sub(/^%/, '')) }.sort_by(&:length).reverse
191
+ return {} if keys.empty?
192
+
193
+ token_rx = /(?<!\\)%(?<width>\*|-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?(?<name>#{keys.join('|')})/
194
+ queues = Hash.new { |h, k| h[k] = [] }
195
+ terminal_width = detected_terminal_width
196
+ raw.each_line do |line|
197
+ tokens = line.to_enum(:scan, token_rx).map { Regexp.last_match.dup }
198
+ next if tokens.empty?
199
+
200
+ literal_width = visible_literal_width(line.gsub(token_rx, ''))
201
+ reserved_width = literal_width
202
+ stretch_tokens = []
203
+
204
+ tokens.each do |token|
205
+ width_token = token['width']
206
+ name = token['name']
207
+ value = placeholders[name] || placeholders["%#{name}"]
208
+ natural = natural_placeholder_width(name, value)
209
+ if width_token == '*'
210
+ if block_placeholder?(name)
211
+ queues[name] << block_placeholder_width(terminal_width, token)
212
+ else
213
+ stretch_tokens << token
214
+ end
215
+ else
216
+ reserved_width += reserved_placeholder_width(width_token, natural)
217
+ end
218
+ end
219
+
220
+ next if stretch_tokens.empty?
221
+
222
+ remaining = terminal_width - reserved_width
223
+ widths = split_stretch_widths(remaining, stretch_tokens.length)
224
+ stretch_tokens.each_with_index do |token, idx|
225
+ queues[token['name']] << widths[idx]
226
+ end
227
+ end
228
+
229
+ queues
230
+ end
231
+
232
+ def split_stretch_widths(remaining, count)
233
+ return [] if count.zero?
234
+ return Array.new(count, 1) if remaining < count
235
+
236
+ base = remaining / count
237
+ extra = remaining % count
238
+ Array.new(count) { |idx| base + (idx < extra ? 1 : 0) }
239
+ end
240
+
241
+ def natural_placeholder_width(name, value)
242
+ # note placeholders are rendered on their own wrapped lines and should not
243
+ # reserve horizontal width on the title line
244
+ return 0 if block_placeholder?(name)
245
+ return 0 unless value.good?
246
+
247
+ normalized = if name =~ /^tags/ && value.respond_to?(:map)
248
+ value.map(&:to_s).join(' ')
249
+ elsif value.respond_to?(:join)
250
+ value.join("\n")
251
+ else
252
+ value.to_s
253
+ end
254
+
255
+ normalized.split("\n").map { |line| visible_literal_width(line) }.max || 0
256
+ end
257
+
258
+ def reserved_placeholder_width(width_token, natural_width)
259
+ return natural_width if width_token.nil? || width_token.empty?
260
+
261
+ minimum = width_token.to_i.abs
262
+ [natural_width, minimum].max
263
+ end
264
+
265
+ def block_placeholder?(name)
266
+ %w[note idnote odnote].include?(name)
267
+ end
268
+
269
+ def block_placeholder_width(terminal_width, token)
270
+ padding = block_placeholder_padding(token)
271
+ [terminal_width - padding, 1].max
272
+ end
273
+
274
+ def block_placeholder_padding(token)
275
+ indent = if token['ichar']
276
+ char = token['ichar'] =~ /t/ ? "\t" : ' '
277
+ char * token['icount'].to_i
278
+ else
279
+ "\t"
280
+ end
281
+
282
+ visible_literal_width("#{indent}#{token['prefix']}")
283
+ end
284
+
285
+ def next_stretch_width(name)
286
+ widths = @stretch_widths[name]
287
+ return 1 if widths.nil? || widths.empty?
288
+
289
+ [widths.shift.to_i, 1].max
290
+ end
291
+
292
+ def visible_literal_width(string)
293
+ visible = strip_template_colors(string).gsub(/\\(%)/, '\1')
294
+ visible.uncolor.length
295
+ end
296
+
297
+ def detected_terminal_width
298
+ if $stdout.tty?
299
+ begin
300
+ require 'io/console'
301
+ console = IO.console
302
+ width = console.winsize[1].to_i if console
303
+ return width if width&.positive?
304
+ rescue StandardError
305
+ # Fall through to tty-screen detection.
306
+ end
307
+ end
308
+
309
+ tty_width = TTY::Screen.columns.to_i
310
+ return tty_width if tty_width.positive?
311
+
312
+ env_width = ENV['COLUMNS'].to_i
313
+ return env_width if env_width.positive?
314
+
315
+ 80
316
+ end
317
+
318
+ def strip_template_colors(string)
319
+ working = string.dup
320
+ string.scan(/(?<!\\)(%((?:[fb]g?)?#[a-fA-F0-9]{6}|[a-z]+))/).each do |color|
321
+ valid_color = color[1].validate_color
322
+ next unless valid_color
323
+
324
+ working.sub!(/(?<!\\)%#{valid_color}/, '')
325
+ end
326
+ working
327
+ end
187
328
  end
188
329
  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.111'
4
+ VERSION = '2.1.115'
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Doing
4
4
  class WWID
5
+ TEMPLATE_WIDTH_PLACEHOLDERS = %w[id tags date interval duration shortdate section title note idnote odnote chompnote].freeze
5
6
  ##
6
7
  ## Display contents of a section based on options
7
8
  ##
@@ -43,7 +44,7 @@ module Doing
43
44
  opt[:order] ||= cfg['order'] || :asc
44
45
  opt[:tag_order] ||= :asc
45
46
  opt[:tags_color] = cfg['tags_color'] || false if opt[:tags_color].nil?
46
- opt[:template] ||= cfg['template']
47
+ opt[:template] ||= resolved_template(cfg)
47
48
  opt[:sort_tags] ||= opt[:tag_sort]
48
49
  end
49
50
 
@@ -263,7 +264,7 @@ module Doing
263
264
  opt[:wrap_width] = cfg['wrap_width']
264
265
  opt[:count] = count
265
266
  opt[:format] = cfg['date_format']
266
- opt[:template] = opt[:template] || cfg['template']
267
+ opt[:template] = opt[:template] || resolved_template(cfg)
267
268
  opt[:order] = :asc
268
269
  end
269
270
 
@@ -372,6 +373,48 @@ module Doing
372
373
 
373
374
  private
374
375
 
376
+ def resolved_template(cfg)
377
+ template = cfg['template']
378
+ return template unless Doing.setting('template_version', 1).to_i >= 2
379
+
380
+ placeholders = cfg['placeholders']
381
+ return template unless placeholders.is_a?(Hash)
382
+
383
+ apply_placeholder_widths(template, placeholders)
384
+ end
385
+
386
+ def apply_placeholder_widths(template, placeholders)
387
+ keys = TEMPLATE_WIDTH_PLACEHOLDERS.map { |k| Regexp.escape(k) }.sort_by(&:length).reverse
388
+ token_rx = /(?<!\\)%(?<width>\*|-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?(?<name>#{keys.join('|')})/i
389
+
390
+ template.gsub(token_rx) do
391
+ m = Regexp.last_match
392
+ name = m['name']
393
+
394
+ width = normalized_placeholder_width(placeholders[name] || placeholders[name.to_sym])
395
+ mchar = m['mchar'] ? "^#{m['mchar']}" : ''
396
+ indent = m['ichar'] ? "#{m['ichar']}#{m['icount']}" : ''
397
+ prefix = m['prefix'] || ''
398
+
399
+ "%#{width}#{mchar}#{indent}#{prefix}#{name}"
400
+ end
401
+ end
402
+
403
+ def normalized_placeholder_width(config)
404
+ width = config['width'] || config[:width] if config.is_a?(Hash)
405
+
406
+ return '' if width.nil?
407
+
408
+ case width.to_s
409
+ when /^stretch$/i
410
+ '*'
411
+ when /^auto$/i, /^$/
412
+ ''
413
+ else
414
+ width.to_i.to_s
415
+ end
416
+ end
417
+
375
418
  ##
376
419
  ## Generate output using available export plugins
377
420
  ##
data/man/doing.1.ronn CHANGED
@@ -199,6 +199,8 @@ The config also contains templates for various command outputs. Include placehol
199
199
 
200
200
  Date formats are based on Ruby [`strftime`](http://www.ruby-doc.org/stdlib-2.1.1/libdoc/date/rdoc/Date.html#method-i-strftime) formatting.
201
201
 
202
+ `%title` and `%note` placeholders accept optional width formatting. Numeric width (for example `%60title`) wraps to that width. Negative width applies a minimum padded width. You can also use `*` as a stretch width marker (for example `%*title`), which expands that placeholder to fill remaining space on the template line based on terminal columns. Stretch width now accounts for the rendered minimum width of trailing fixed placeholders, so `%*title` expands up to where right-side content begins and that trailing content can sit at the right edge. Placeholders without any width marker keep natural rendered width and do not expand. When multiple `%*...` placeholders are on one line, remaining width is split evenly from left to right.
203
+
202
204
  My normal template for the `recent` command looks like this:
203
205
 
204
206
  recent:
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.111
4
+ version: 2.1.115
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra