howzit 2.1.24 → 2.1.26

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9539bebf0157985ac2d5a035923c1e7f78e942961f79a0729d20920d78c38f3a
4
- data.tar.gz: 0f16b31308c0e78f7d13e140363d2f62cd8da38658c5d1a9e29721ea0e6dda5f
3
+ metadata.gz: 19fc185635709d53c4d7dcf0b437d4ec6b00cdd341d734e4655ef4e8e1dbfd3a
4
+ data.tar.gz: 9305e6edf7a8c4f7bf6d2d6a4a1aed14313aac73b658595a877314e929c15c8b
5
5
  SHA512:
6
- metadata.gz: bb8ddc83529c66ddbcc1a71be7bdcdbf118427f08a206d7bc5a02e0b5b1a363d01f321e8865eab8f44a8b871dd8a8134e20e4d1bbd3a880cbbdc7c87f688062c
7
- data.tar.gz: 3a8501d2c2c7b7a35ac82701353b8b3162eb6b6b5890c98c5b171eb8be56bdaf9fc8cd841ef8610ca95482c2fe820964aae98c0945be8eaae6e2a1eb0b3432de
6
+ metadata.gz: a35475dfaf7aa381157964466046ff8c94b0a4282bd9bbb8c067b7d344527d8ba95a296934ce935375111c4c255122cd3c684829d761b4962c517ea6dfadc14b
7
+ data.tar.gz: 41ae72b53d9602fb9b6df8231a4d1db8d9f134b78460e2243e8afaf616528279d0e7b08d68820a330994935579186058838a5d5e5c288b809aa46d0670b71b0f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ### 2.1.26
2
+
3
+ 2025-12-26 04:53
4
+
5
+ #### FIXED
6
+
7
+ - Bash script variables in run blocks now preserve ${VAR} syntax when the variable is not defined by howzit, allowing bash to handle them normally instead of being replaced with empty strings.
8
+
9
+ ### 2.1.25
10
+
11
+ 2025-12-19 07:41
12
+
13
+ #### NEW
14
+
15
+ - Added default metadata support to automatically run topics when executing `howzit --run` with no arguments. Supports multiple comma-separated topics with optional bracketed arguments (e.g., `default: Build Project, Run Tests[verbose]`).
16
+
17
+ #### FIXED
18
+
19
+ - Task titles are now correctly displayed in run reports and "Running" messages instead of showing the command. When a title is provided after @run or @include directives (e.g., `@run(ls) List directory`), the title is used throughout.
20
+ - Fixed color code interpretation issues when task or topic names contain dollar signs or braces. These characters are now properly escaped to prevent them from being interpreted as color template codes.
21
+ - Fixed issue where task titles containing braces would show literal `{x}` in output due to color template parsing.
22
+
1
23
  ### 2.1.24
2
24
 
3
25
  2025-12-13 07:11
@@ -955,18 +955,32 @@ module Howzit
955
955
  titles.each { |title| topic_matches.push(find_topic(title)[0]) }
956
956
  process_topic_matches(topic_matches, output)
957
957
  elsif !Howzit.cli_args.empty?
958
- # Collect all topic matches first (showing menus as needed)
959
- search = topic_search_terms_from_cli
960
- topic_matches = collect_topic_matches(search, output)
961
- process_topic_matches(topic_matches, output)
958
+ # Check if first arg is "default"
959
+ if Howzit.options[:run] && Howzit.cli_args[0].downcase == 'default'
960
+ process_default_metadata(output)
961
+ else
962
+ # Collect all topic matches first (showing menus as needed)
963
+ search = topic_search_terms_from_cli
964
+ topic_matches = collect_topic_matches(search, output)
965
+ process_topic_matches(topic_matches, output)
966
+ end
962
967
  else
963
- # No arguments - show all topics
968
+ # No arguments
964
969
  if Howzit.options[:run]
965
- Howzit.run_log = []
966
- Howzit.multi_topic_run = topics.length > 1
970
+ # Check for default metadata when running with no args
971
+ if @metadata.key?('default')
972
+ process_default_metadata(output)
973
+ else
974
+ Howzit.run_log = []
975
+ Howzit.multi_topic_run = topics.length > 1
976
+ topics.each { |k| output.push(process_topic(k, false, single: false)) }
977
+ finalize_output(output)
978
+ end
979
+ else
980
+ # Show all topics
981
+ topics.each { |k| output.push(process_topic(k, false, single: false)) }
982
+ finalize_output(output)
967
983
  end
968
- topics.each { |k| output.push(process_topic(k, false, single: false)) }
969
- finalize_output(output)
970
984
  end
971
985
  end
972
986
 
@@ -1060,6 +1074,77 @@ module Howzit
1060
1074
  finalize_output(output)
1061
1075
  end
1062
1076
 
1077
+ ##
1078
+ ## Process default metadata and run the specified topics
1079
+ ##
1080
+ ## @param output [Array] Output array
1081
+ ##
1082
+ def process_default_metadata(output)
1083
+ default_value = @metadata['default']
1084
+ return if default_value.nil? || default_value.strip.empty?
1085
+
1086
+ Howzit.run_log = []
1087
+ default_topics = parse_default_metadata(default_value)
1088
+ Howzit.multi_topic_run = default_topics.length > 1
1089
+
1090
+ topic_specs = []
1091
+ default_topics.each do |topic_spec|
1092
+ topic_name, args = parse_topic_with_args(topic_spec)
1093
+ matches = find_topic(topic_name)
1094
+ if matches.empty?
1095
+ output.push(%({bR}ERROR:{xr} No topic match found for {bw}#{topic_name}{x}\n).c)
1096
+ else
1097
+ # Store topic and its arguments together (take first match, like @include does)
1098
+ topic_specs << [matches[0], args]
1099
+ end
1100
+ end
1101
+
1102
+ if topic_specs.empty?
1103
+ Util.show(output.join("\n"), { color: true, highlight: false, paginate: false, wrap: 0 })
1104
+ Process.exit 1
1105
+ end
1106
+
1107
+ # Run each topic with its specific arguments
1108
+ topic_specs.each do |topic_match, args|
1109
+ # Set arguments if provided, otherwise clear them
1110
+ if args && !args.empty?
1111
+ Howzit.arguments = args.split(/ *, */).map(&:render_arguments)
1112
+ else
1113
+ Howzit.arguments = []
1114
+ end
1115
+ output.push(process_topic(topic_match, Howzit.options[:run], single: true))
1116
+ end
1117
+ finalize_output(output)
1118
+ end
1119
+
1120
+ ##
1121
+ ## Parse default metadata value into individual topic specifications
1122
+ ##
1123
+ ## @param default_value [String] The default metadata value
1124
+ ##
1125
+ ## @return [Array] Array of topic specification strings
1126
+ ##
1127
+ def parse_default_metadata(default_value)
1128
+ default_value.strip.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
1129
+ end
1130
+
1131
+ ##
1132
+ ## Parse a topic specification that may include bracketed arguments
1133
+ ##
1134
+ ## @param topic_spec [String] Topic specification like "Run Snippet[document.md]"
1135
+ ##
1136
+ ## @return [Array] [topic_name, args_string]
1137
+ ##
1138
+ def parse_topic_with_args(topic_spec)
1139
+ if topic_spec =~ /\[(.*?)\]$/
1140
+ args = Regexp.last_match(1)
1141
+ topic_name = topic_spec.sub(/\[.*?\]$/, '').strip
1142
+ [topic_name, args]
1143
+ else
1144
+ [topic_spec.strip, nil]
1145
+ end
1146
+ end
1147
+
1063
1148
  ##
1064
1149
  ## Finalize and display output with run summary if applicable
1065
1150
  ##
data/lib/howzit/colors.rb CHANGED
@@ -272,7 +272,7 @@ module Howzit
272
272
  def template(input)
273
273
  input = input.join(' ') if input.is_a? Array
274
274
  fmt = input.gsub(/%/, '%%')
275
- fmt = fmt.gsub(/(?<!\\u|\$)\{(\w+)\}/i) do
275
+ fmt = fmt.gsub(/(?<!\\u|\$|\\\\)\{(\w+)\}/i) do
276
276
  m = Regexp.last_match(1)
277
277
  if m =~ /^[wkglycmrWKGLYCMRdbuix]+$/
278
278
  m.split('').map { |c| "%<#{c}>s" }.join('')
@@ -287,7 +287,9 @@ module Howzit
287
287
  Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
288
288
  d: dark, b: bold, u: underline, i: italic, x: reset }
289
289
 
290
- fmt.empty? ? input : format(fmt, colors)
290
+ result = fmt.empty? ? input : format(fmt, colors)
291
+ # Unescape braces that were escaped to prevent color code interpretation
292
+ result.gsub(/\\\{/, '{').gsub(/\\\}/, '}')
291
293
  end
292
294
  end
293
295
 
@@ -29,8 +29,14 @@ module Howzit
29
29
  def format_line(entry, prefix_topic)
30
30
  symbol = entry[:success] ? '✅' : '❌'
31
31
  parts = ["#{symbol} "]
32
- parts << "{bw}#{entry[:topic]}{x}: " if prefix_topic && entry[:topic] && !entry[:topic].empty?
33
- parts << "{by}#{entry[:task]}{x}"
32
+ if prefix_topic && entry[:topic] && !entry[:topic].empty?
33
+ # Escape braces in topic name to prevent color code interpretation
34
+ topic_escaped = entry[:topic].gsub(/\{/, '\\{').gsub(/\}/, '\\}')
35
+ parts << "{bw}#{topic_escaped}{x}: "
36
+ end
37
+ # Escape braces in task name to prevent color code interpretation
38
+ task_escaped = entry[:task].gsub(/\{/, '\\{').gsub(/\}/, '\\}')
39
+ parts << "{by}#{task_escaped}{x}"
34
40
  unless entry[:success]
35
41
  reason = entry[:exit_status] ? "exit code #{entry[:exit_status]}" : 'failed'
36
42
  parts << " {br}(#{reason}){x}"
@@ -61,7 +67,7 @@ module Howzit
61
67
  table_lines.join("\n")
62
68
  end
63
69
 
64
- def table_row_colored(status, task, task_plain, status_width, task_width)
70
+ def table_row_colored(status, task, task_plain, _status_width, task_width)
65
71
  task_padding = task_width - task_plain.length
66
72
 
67
73
  "| #{status} | #{task}#{' ' * task_padding} |"
@@ -76,11 +82,15 @@ module Howzit
76
82
  task_parts_plain = []
77
83
 
78
84
  if prefix_topic && entry[:topic] && !entry[:topic].empty?
79
- task_parts << "{bw}#{entry[:topic]}{x}: "
85
+ # Escape braces in topic name to prevent color code interpretation
86
+ topic_escaped = entry[:topic].gsub(/\{/, '\\{').gsub(/\}/, '\\}')
87
+ task_parts << "{bw}#{topic_escaped}{x}: "
80
88
  task_parts_plain << "#{entry[:topic]}: "
81
89
  end
82
90
 
83
- task_parts << "{by}#{entry[:task]}{x}"
91
+ # Escape braces in task name to prevent color code interpretation
92
+ task_escaped = entry[:task].gsub(/\{/, '\\{').gsub(/\}/, '\\}')
93
+ task_parts << "{by}#{task_escaped}{x}"
84
94
  task_parts_plain << entry[:task]
85
95
 
86
96
  unless entry[:success]
@@ -337,7 +337,14 @@ module Howzit
337
337
  gsub!(/\$\{(?<name>[A-Z0-9_]+(?::.*?)?)\}/i) do
338
338
  m = Regexp.last_match
339
339
  arg, default = m['name'].split(/:/).map(&:strip)
340
- Howzit.named_arguments.key?(arg) && !Howzit.named_arguments[arg].nil? ? Howzit.named_arguments[arg] : default
340
+ if Howzit.named_arguments && Howzit.named_arguments.key?(arg) && !Howzit.named_arguments[arg].nil?
341
+ Howzit.named_arguments[arg]
342
+ elsif default
343
+ default
344
+ else
345
+ # Preserve the original ${VAR} syntax if variable is not defined and no default provided
346
+ m[0]
347
+ end
341
348
  end
342
349
  end
343
350
 
data/lib/howzit/task.rb CHANGED
@@ -26,7 +26,7 @@ module Howzit
26
26
  @arguments = attributes[:arguments] || []
27
27
 
28
28
  @type = attributes[:type] || :run
29
- @title = attributes[:title] || nil
29
+ @title = attributes[:title].nil? ? nil : attributes[:title].to_s
30
30
  @parent = attributes[:parent] || nil
31
31
 
32
32
  @action = attributes[:action].render_arguments || nil
@@ -98,8 +98,19 @@ module Howzit
98
98
  ## Execute a run task
99
99
  ##
100
100
  def run_run
101
- title = Howzit.options[:show_all_code] ? @action : @title
102
- Howzit.console.info("#{@prefix}{bg}Running {bw}#{title}{x}".c)
101
+ # If a title was explicitly provided (different from action), always use it
102
+ # Otherwise, use action (or respect show_all_code if no title)
103
+ display_title = if @title && !@title.empty? && @title != @action
104
+ # Title was explicitly provided, use it
105
+ @title
106
+ elsif Howzit.options[:show_all_code]
107
+ # No explicit title, show code if requested
108
+ @action
109
+ else
110
+ # No explicit title, use title if available (might be same as action), otherwise action
111
+ @title && !@title.empty? ? @title : @action
112
+ end
113
+ Howzit.console.info("#{@prefix}{bg}Running {bw}#{display_title}{x}".c)
103
114
  ENV['HOWZIT_SCRIPTS'] = File.expand_path('~/.config/howzit/scripts')
104
115
  res = system(@action)
105
116
  update_last_status(res ? 0 : 1)
@@ -110,8 +121,19 @@ module Howzit
110
121
  ## Execute a copy task
111
122
  ##
112
123
  def run_copy
113
- title = Howzit.options[:show_all_code] ? @action : @title
114
- Howzit.console.info("#{@prefix}{bg}Copied {bw}#{title}{bg} to clipboard{x}".c)
124
+ # If a title was explicitly provided (different from action), always use it
125
+ # Otherwise, use action (or respect show_all_code if no title)
126
+ display_title = if @title && !@title.empty? && @title != @action
127
+ # Title was explicitly provided, use it
128
+ @title
129
+ elsif Howzit.options[:show_all_code]
130
+ # No explicit title, show code if requested
131
+ @action
132
+ else
133
+ # No explicit title, use title if available (might be same as action), otherwise action
134
+ @title && !@title.empty? ? @title : @action
135
+ end
136
+ Howzit.console.info("#{@prefix}{bg}Copied {bw}#{display_title}{bg} to clipboard{x}".c)
115
137
  Util.os_copy(@action)
116
138
  @last_status = 0
117
139
  true
data/lib/howzit/topic.rb CHANGED
@@ -288,11 +288,18 @@ module Howzit
288
288
  def define_task_args(keys)
289
289
  cmd = keys[:cmd]
290
290
  obj = keys[:action]
291
- title = keys[:title].nil? ? obj : keys[:title].strip
292
- title = Howzit.options[:show_all_code] ? obj : title
291
+ # Extract and clean the title
292
+ raw_title = keys[:title]
293
+ # Determine the title: use provided title if available, otherwise use action
294
+ title = if raw_title.nil? || raw_title.to_s.strip.empty?
295
+ obj
296
+ else
297
+ raw_title.to_s.strip
298
+ end
299
+ # Store the actual title (not overridden by show_all_code - that's only for display)
293
300
  task_args = { type: :include,
294
301
  arguments: nil,
295
- title: title,
302
+ title: title.dup, # Make a copy to avoid reference issues
296
303
  action: obj,
297
304
  parent: self }
298
305
  case cmd
@@ -303,6 +310,7 @@ module Howzit
303
310
  Howzit.arguments = args
304
311
  arguments
305
312
  title.sub!(/ *\[.*?\] *$/, '')
313
+ task_args[:title] = title
306
314
  end
307
315
 
308
316
  task_args[:type] = :include
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Howzit
5
5
  # Current Howzit version.
6
- VERSION = '2.1.24'
6
+ VERSION = '2.1.26'
7
7
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'StringUtils' do
6
+ describe '#render_named_placeholders' do
7
+ before do
8
+ Howzit.named_arguments = {}
9
+ end
10
+
11
+ it 'preserves ${VAR} syntax when variable is not defined' do
12
+ str = 'echo ${MY_VAR}'.dup
13
+ str.render_named_placeholders
14
+ expect(str).to eq('echo ${MY_VAR}')
15
+ end
16
+
17
+ it 'preserves ${VAR} syntax for multiple undefined variables' do
18
+ str = 'echo ${VAR1} and ${VAR2}'.dup
19
+ str.render_named_placeholders
20
+ expect(str).to eq('echo ${VAR1} and ${VAR2}')
21
+ end
22
+
23
+ it 'replaces ${VAR} with value when variable is defined' do
24
+ Howzit.named_arguments = { 'MY_VAR' => 'hello' }
25
+ str = 'echo ${MY_VAR}'.dup
26
+ str.render_named_placeholders
27
+ expect(str).to eq('echo hello')
28
+ end
29
+
30
+ it 'uses default value when variable is not defined but default is provided' do
31
+ str = 'echo ${MY_VAR:default_value}'.dup
32
+ str.render_named_placeholders
33
+ expect(str).to eq('echo default_value')
34
+ end
35
+
36
+ it 'replaces variable when defined even if default is provided' do
37
+ Howzit.named_arguments = { 'MY_VAR' => 'actual_value' }
38
+ str = 'echo ${MY_VAR:default_value}'.dup
39
+ str.render_named_placeholders
40
+ expect(str).to eq('echo actual_value')
41
+ end
42
+
43
+ it 'preserves ${VAR} in bash script blocks' do
44
+ script = <<~SCRIPT
45
+ #!/bin/bash
46
+ echo "The value is ${ENV_VAR}"
47
+ echo "Another ${OTHER_VAR}"
48
+ SCRIPT
49
+ str = script.dup
50
+ str.render_named_placeholders
51
+ expect(str).to eq(script)
52
+ end
53
+
54
+ it 'handles mixed defined and undefined variables' do
55
+ Howzit.named_arguments = { 'DEFINED_VAR' => 'value1' }
56
+ str = 'echo ${DEFINED_VAR} and ${UNDEFINED_VAR}'.dup
57
+ str.render_named_placeholders
58
+ expect(str).to eq('echo value1 and ${UNDEFINED_VAR}')
59
+ end
60
+
61
+ it 'handles nil named_arguments gracefully' do
62
+ Howzit.named_arguments = nil
63
+ str = 'echo ${MY_VAR}'.dup
64
+ expect { str.render_named_placeholders }.not_to raise_error
65
+ expect(str).to eq('echo ${MY_VAR}')
66
+ end
67
+ end
68
+
69
+ describe '#render_arguments' do
70
+ before do
71
+ Howzit.named_arguments = {}
72
+ Howzit.arguments = nil
73
+ end
74
+
75
+ it 'preserves ${VAR} syntax through render_arguments' do
76
+ str = 'echo ${BASH_VAR}'.dup
77
+ result = str.render_arguments
78
+ expect(result).to eq('echo ${BASH_VAR}')
79
+ end
80
+ end
81
+ end
82
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: howzit
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.24
4
+ version: 2.1.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -315,6 +315,7 @@ files:
315
315
  - spec/ruby_gem_spec.rb
316
316
  - spec/run_report_spec.rb
317
317
  - spec/spec_helper.rb
318
+ - spec/stringutils_spec.rb
318
319
  - spec/task_spec.rb
319
320
  - spec/topic_spec.rb
320
321
  - spec/util_spec.rb
@@ -349,6 +350,7 @@ test_files:
349
350
  - spec/ruby_gem_spec.rb
350
351
  - spec/run_report_spec.rb
351
352
  - spec/spec_helper.rb
353
+ - spec/stringutils_spec.rb
352
354
  - spec/task_spec.rb
353
355
  - spec/topic_spec.rb
354
356
  - spec/util_spec.rb