howzit 2.1.28 → 2.1.29
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 +4 -4
- data/CHANGELOG.md +47 -0
- data/Rakefile +4 -3
- data/bin/howzit +65 -65
- data/howzit.gemspec +1 -1
- data/lib/howzit/buildnote.rb +4 -8
- data/lib/howzit/colors.rb +50 -22
- data/lib/howzit/condition_evaluator.rb +307 -0
- data/lib/howzit/conditional_content.rb +96 -0
- data/lib/howzit/config.rb +15 -3
- data/lib/howzit/console_logger.rb +14 -2
- data/lib/howzit/prompt.rb +20 -12
- data/lib/howzit/run_report.rb +1 -1
- data/lib/howzit/script_comm.rb +105 -0
- data/lib/howzit/stringutils.rb +4 -4
- data/lib/howzit/task.rb +11 -2
- data/lib/howzit/topic.rb +29 -9
- data/lib/howzit/util.rb +11 -3
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +3 -0
- data/spec/condition_evaluator_spec.rb +261 -0
- data/spec/conditional_blocks_integration_spec.rb +159 -0
- data/spec/conditional_content_spec.rb +296 -0
- data/spec/script_comm_spec.rb +303 -0
- data/spec/spec_helper.rb +3 -1
- metadata +14 -3
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
|
|
5
|
+
module Howzit
|
|
6
|
+
# Condition Evaluator module
|
|
7
|
+
# Handles evaluation of @if/@unless conditions
|
|
8
|
+
module ConditionEvaluator
|
|
9
|
+
class << self
|
|
10
|
+
##
|
|
11
|
+
## Evaluate a condition expression
|
|
12
|
+
##
|
|
13
|
+
## @param condition [String] The condition to evaluate
|
|
14
|
+
## @param context [Hash] Context with metadata, arguments, etc.
|
|
15
|
+
##
|
|
16
|
+
## @return [Boolean] Result of condition evaluation
|
|
17
|
+
##
|
|
18
|
+
def evaluate(condition, context = {})
|
|
19
|
+
condition = condition.strip
|
|
20
|
+
|
|
21
|
+
# Handle negation with 'not' or '!'
|
|
22
|
+
negated = false
|
|
23
|
+
if condition =~ /^(not\s+|!)/
|
|
24
|
+
negated = true
|
|
25
|
+
condition = condition.sub(/^(not\s+|!)/, '').strip
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result = evaluate_condition(condition, context)
|
|
29
|
+
negated ? !result : result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
## Evaluate a single condition (without negation)
|
|
36
|
+
##
|
|
37
|
+
def evaluate_condition(condition, context)
|
|
38
|
+
# Handle special conditions FIRST to avoid false matches with comparison patterns
|
|
39
|
+
# Check file contents before other patterns since it has arguments and operators
|
|
40
|
+
if condition =~ /^file\s+contents\s+(.+?)\s+(\*\*=|\*=|\^=|\$=|==|!=|=~)\s*(.+)$/i
|
|
41
|
+
return evaluate_file_contents(condition, context)
|
|
42
|
+
# Check file/dir/topic exists before other patterns since they have arguments
|
|
43
|
+
elsif condition =~ /^(file\s+exists|dir\s+exists|topic\s+exists)\s+(.+)$/i
|
|
44
|
+
return evaluate_special(condition, context)
|
|
45
|
+
elsif condition =~ /^(git\s+dirty|git\s+clean)$/i
|
|
46
|
+
return evaluate_special(condition, context)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Handle =~ regex comparisons separately (before string == to avoid conflicts)
|
|
50
|
+
if (match = condition.match(%r{^(.+?)\s*=~\s*/(.+)/$}))
|
|
51
|
+
left = match[1].strip
|
|
52
|
+
pattern = match[2].strip
|
|
53
|
+
|
|
54
|
+
left_val = get_value(left, context)
|
|
55
|
+
return false if left_val.nil?
|
|
56
|
+
|
|
57
|
+
!!(left_val.to_s =~ /#{pattern}/)
|
|
58
|
+
# Handle comparisons with ==, !=, >, >=, <, <=
|
|
59
|
+
# Determine if numeric or string comparison based on values
|
|
60
|
+
elsif (match = condition.match(/^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/))
|
|
61
|
+
left = match[1].strip
|
|
62
|
+
operator = match[2]
|
|
63
|
+
right = match[3].strip
|
|
64
|
+
|
|
65
|
+
left_val = get_value(left, context)
|
|
66
|
+
# If get_value returned nil, try using the original string as a literal
|
|
67
|
+
left_val = left if left_val.nil? && numeric_value(left)
|
|
68
|
+
right_val = get_value(right, context)
|
|
69
|
+
# If get_value returned nil, try using the original string as a literal
|
|
70
|
+
right_val = right if right_val.nil? && numeric_value(right)
|
|
71
|
+
|
|
72
|
+
# Try to convert to numbers
|
|
73
|
+
left_num = numeric_value(left_val)
|
|
74
|
+
right_num = numeric_value(right_val)
|
|
75
|
+
|
|
76
|
+
# If both are numeric, use numeric comparison
|
|
77
|
+
if left_num && right_num
|
|
78
|
+
case operator
|
|
79
|
+
when '=='
|
|
80
|
+
left_num == right_num
|
|
81
|
+
when '!='
|
|
82
|
+
left_num != right_num
|
|
83
|
+
when '>'
|
|
84
|
+
left_num > right_num
|
|
85
|
+
when '>='
|
|
86
|
+
left_num >= right_num
|
|
87
|
+
when '<'
|
|
88
|
+
left_num < right_num
|
|
89
|
+
when '<='
|
|
90
|
+
left_num <= right_num
|
|
91
|
+
else
|
|
92
|
+
false
|
|
93
|
+
end
|
|
94
|
+
# Otherwise use string comparison for == and !=, or return false for others
|
|
95
|
+
else
|
|
96
|
+
case operator
|
|
97
|
+
when '=='
|
|
98
|
+
# Handle nil comparisons
|
|
99
|
+
left_val.nil? == right_val.nil? && (left_val.nil? || left_val.to_s == right_val.to_s)
|
|
100
|
+
when '!='
|
|
101
|
+
left_val.nil? != right_val.nil? || (!left_val.nil? && !right_val.nil? && left_val.to_s != right_val.to_s)
|
|
102
|
+
else
|
|
103
|
+
# For >, >=, <, <=, return false if not numeric
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
# Handle string-only comparisons: **= (fuzzy match), *= (contains), ^= (starts with), $= (ends with)
|
|
108
|
+
# Note: **= must come before *= in the regex to avoid matching *= first
|
|
109
|
+
elsif (match = condition.match(/^(.+?)\s*(\*\*=|\*=|\^=|\$=)\s*(.+)$/))
|
|
110
|
+
left = match[1].strip
|
|
111
|
+
operator = match[2]
|
|
112
|
+
right = match[3].strip
|
|
113
|
+
|
|
114
|
+
left_val = get_value(left, context)
|
|
115
|
+
right_val = get_value(right, context)
|
|
116
|
+
# If right side is nil (variable not found), treat it as a literal string
|
|
117
|
+
right_val = right if right_val.nil?
|
|
118
|
+
|
|
119
|
+
return false if left_val.nil? || right_val.nil?
|
|
120
|
+
|
|
121
|
+
case operator
|
|
122
|
+
when '*='
|
|
123
|
+
left_val.to_s.include?(right_val.to_s)
|
|
124
|
+
when '^='
|
|
125
|
+
left_val.to_s.start_with?(right_val.to_s)
|
|
126
|
+
when '$='
|
|
127
|
+
left_val.to_s.end_with?(right_val.to_s)
|
|
128
|
+
when '**='
|
|
129
|
+
# Fuzzy match: split search string into chars and join with .*? for regex
|
|
130
|
+
pattern = "^.*?#{right_val.to_s.split('').map { |c| Regexp.escape(c) }.join('.*?')}.*?$"
|
|
131
|
+
!!(left_val.to_s =~ /#{pattern}/)
|
|
132
|
+
else
|
|
133
|
+
false
|
|
134
|
+
end
|
|
135
|
+
# Simple existence check (just a variable name)
|
|
136
|
+
else
|
|
137
|
+
val = get_value(condition, context)
|
|
138
|
+
!val.nil? && val.to_s != ''
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
## Get value from various sources
|
|
144
|
+
##
|
|
145
|
+
def get_value(expr, context)
|
|
146
|
+
expr = expr.strip
|
|
147
|
+
|
|
148
|
+
# Remove quotes if present
|
|
149
|
+
return Regexp.last_match(1) if expr =~ /^["'](.+)["']$/
|
|
150
|
+
|
|
151
|
+
# Remove ${} wrapper if present (for consistency with variable substitution syntax)
|
|
152
|
+
expr = Regexp.last_match(1) if expr =~ /^\$\{(.+)\}$/
|
|
153
|
+
|
|
154
|
+
# Check positional arguments
|
|
155
|
+
if expr =~ /^\$(\d+)$/
|
|
156
|
+
idx = Regexp.last_match(1).to_i - 1
|
|
157
|
+
return Howzit.arguments[idx] if Howzit.arguments && Howzit.arguments[idx]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Check named arguments
|
|
161
|
+
return Howzit.named_arguments[expr.to_sym] if Howzit.named_arguments&.key?(expr.to_sym)
|
|
162
|
+
return Howzit.named_arguments[expr] if Howzit.named_arguments&.key?(expr)
|
|
163
|
+
|
|
164
|
+
# Check metadata (from context only, to avoid circular dependencies)
|
|
165
|
+
metadata = context[:metadata]
|
|
166
|
+
return metadata[expr] if metadata&.key?(expr)
|
|
167
|
+
return metadata[expr.downcase] if metadata&.key?(expr.downcase)
|
|
168
|
+
|
|
169
|
+
# Check environment variables
|
|
170
|
+
return ENV[expr] if ENV.key?(expr)
|
|
171
|
+
return ENV[expr.upcase] if ENV.key?(expr.upcase)
|
|
172
|
+
|
|
173
|
+
# Check for special values: cwd, working directory
|
|
174
|
+
return Dir.pwd if expr =~ /^(cwd|working\s+directory)$/i
|
|
175
|
+
|
|
176
|
+
# Return nil if nothing matched (variable is undefined)
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
##
|
|
181
|
+
## Convert value to numeric if possible
|
|
182
|
+
##
|
|
183
|
+
def numeric_value(val)
|
|
184
|
+
return val if val.is_a?(Numeric)
|
|
185
|
+
|
|
186
|
+
str = val.to_s.strip
|
|
187
|
+
return nil if str.empty?
|
|
188
|
+
|
|
189
|
+
# Try integer first
|
|
190
|
+
return str.to_i if str =~ /^-?\d+$/
|
|
191
|
+
|
|
192
|
+
# Try float
|
|
193
|
+
return str.to_f if str =~ /^-?\d+\.\d+$/
|
|
194
|
+
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
##
|
|
199
|
+
## Evaluate special conditions
|
|
200
|
+
##
|
|
201
|
+
def evaluate_special(condition, context)
|
|
202
|
+
condition = condition.downcase.strip
|
|
203
|
+
|
|
204
|
+
if condition =~ /^git\s+dirty$/i
|
|
205
|
+
git_dirty?
|
|
206
|
+
elsif condition =~ /^git\s+clean$/i
|
|
207
|
+
!git_dirty?
|
|
208
|
+
elsif (match = condition.match(/^file\s+exists\s+(.+)$/i))
|
|
209
|
+
file = match[1].strip
|
|
210
|
+
# get_value returns nil if not found, so use original path if nil
|
|
211
|
+
file_val = get_value(file, context)
|
|
212
|
+
file_path = file_val.nil? ? file : file_val.to_s
|
|
213
|
+
File.exist?(file_path) && !File.directory?(file_path)
|
|
214
|
+
elsif (match = condition.match(/^dir\s+exists\s+(.+)$/i))
|
|
215
|
+
dir = match[1].strip
|
|
216
|
+
dir_val = get_value(dir, context)
|
|
217
|
+
dir_path = dir_val.nil? ? dir : dir_val.to_s
|
|
218
|
+
File.directory?(dir_path)
|
|
219
|
+
elsif (match = condition.match(/^topic\s+exists\s+(.+)$/i))
|
|
220
|
+
topic_name = match[1].strip
|
|
221
|
+
topic_name = get_value(topic_name, context).to_s
|
|
222
|
+
find_topic(topic_name)
|
|
223
|
+
else
|
|
224
|
+
false
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
##
|
|
229
|
+
## Check if git repository is dirty
|
|
230
|
+
##
|
|
231
|
+
def git_dirty?
|
|
232
|
+
return false unless `which git`.strip != ''
|
|
233
|
+
|
|
234
|
+
Dir.chdir(Dir.pwd) do
|
|
235
|
+
`git diff --quiet 2>/dev/null`
|
|
236
|
+
$CHILD_STATUS.exitstatus != 0
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
##
|
|
241
|
+
## Check if topic exists in buildnote
|
|
242
|
+
##
|
|
243
|
+
def find_topic(topic_name)
|
|
244
|
+
return false unless Howzit.buildnote
|
|
245
|
+
|
|
246
|
+
matches = Howzit.buildnote.find_topic(topic_name)
|
|
247
|
+
!matches.empty?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
##
|
|
251
|
+
## Evaluate file contents condition
|
|
252
|
+
## Reads file and performs string comparison
|
|
253
|
+
##
|
|
254
|
+
def evaluate_file_contents(condition, context)
|
|
255
|
+
match = condition.match(/^file\s+contents\s+(.+?)\s+(\*\*=|\*=|\^=|\$=|==|!=|=~)\s*(.+)$/i)
|
|
256
|
+
return false unless match
|
|
257
|
+
|
|
258
|
+
file_path = match[1].strip
|
|
259
|
+
operator = match[2]
|
|
260
|
+
search_value = match[3].strip
|
|
261
|
+
|
|
262
|
+
# Resolve file path (could be a variable)
|
|
263
|
+
file_path_val = get_value(file_path, context)
|
|
264
|
+
file_path = file_path_val.nil? ? file_path : file_path_val.to_s
|
|
265
|
+
|
|
266
|
+
# Resolve search value (could be a variable)
|
|
267
|
+
search_val = get_value(search_value, context)
|
|
268
|
+
search_val = search_val.nil? ? search_value : search_val.to_s
|
|
269
|
+
|
|
270
|
+
# Read file contents
|
|
271
|
+
return false unless File.exist?(file_path) && !File.directory?(file_path)
|
|
272
|
+
|
|
273
|
+
begin
|
|
274
|
+
file_contents = File.read(file_path).strip
|
|
275
|
+
rescue StandardError
|
|
276
|
+
return false
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Perform comparison based on operator
|
|
280
|
+
case operator
|
|
281
|
+
when '=='
|
|
282
|
+
file_contents == search_val.to_s
|
|
283
|
+
when '!='
|
|
284
|
+
file_contents != search_val.to_s
|
|
285
|
+
when '*='
|
|
286
|
+
file_contents.include?(search_val.to_s)
|
|
287
|
+
when '^='
|
|
288
|
+
file_contents.start_with?(search_val.to_s)
|
|
289
|
+
when '$='
|
|
290
|
+
file_contents.end_with?(search_val.to_s)
|
|
291
|
+
when '**='
|
|
292
|
+
# Fuzzy match: split search string into chars and join with .*? for regex
|
|
293
|
+
pattern = "^.*?#{search_val.to_s.split('').map { |c| Regexp.escape(c) }.join('.*?')}.*?$"
|
|
294
|
+
!!(file_contents =~ /#{pattern}/)
|
|
295
|
+
when '=~'
|
|
296
|
+
# Regex match - search_value should be a regex pattern
|
|
297
|
+
pattern = search_val.to_s
|
|
298
|
+
# Remove leading/trailing slashes if present
|
|
299
|
+
pattern = pattern[1..-2] if pattern.start_with?('/') && pattern.end_with?('/')
|
|
300
|
+
!!(file_contents =~ /#{pattern}/)
|
|
301
|
+
else
|
|
302
|
+
false
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Howzit
|
|
4
|
+
# Conditional Content processor
|
|
5
|
+
# Handles @if/@unless/@end blocks in topic content
|
|
6
|
+
module ConditionalContent
|
|
7
|
+
class << self
|
|
8
|
+
##
|
|
9
|
+
## Process conditional blocks in content
|
|
10
|
+
##
|
|
11
|
+
## @param content [String] The content to process
|
|
12
|
+
## @param context [Hash] Context for condition evaluation
|
|
13
|
+
##
|
|
14
|
+
## @return [String] Content with conditional blocks processed
|
|
15
|
+
##
|
|
16
|
+
def process(content, context = {})
|
|
17
|
+
lines = content.split(/\n/)
|
|
18
|
+
output = []
|
|
19
|
+
condition_stack = []
|
|
20
|
+
# Track if any condition in the current chain has been true
|
|
21
|
+
# This is used for @elsif and @else to know if a previous branch matched
|
|
22
|
+
chain_matched_stack = []
|
|
23
|
+
|
|
24
|
+
lines.each do |line|
|
|
25
|
+
# Check for @if or @unless
|
|
26
|
+
if line =~ /^@(if|unless)\s+(.+)$/i
|
|
27
|
+
directive = Regexp.last_match(1).downcase
|
|
28
|
+
condition = Regexp.last_match(2).strip
|
|
29
|
+
|
|
30
|
+
# Evaluate condition
|
|
31
|
+
result = ConditionEvaluator.evaluate(condition, context)
|
|
32
|
+
# For @unless, negate the result
|
|
33
|
+
result = !result if directive == 'unless'
|
|
34
|
+
|
|
35
|
+
condition_stack << result
|
|
36
|
+
chain_matched_stack << result
|
|
37
|
+
|
|
38
|
+
# Don't include the @if/@unless line itself
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check for @elsif
|
|
43
|
+
if line =~ /^@elsif\s+(.+)$/i
|
|
44
|
+
condition = Regexp.last_match(1).strip
|
|
45
|
+
|
|
46
|
+
# If previous condition in chain was true, this branch is false
|
|
47
|
+
# Otherwise, evaluate the condition
|
|
48
|
+
if !condition_stack.empty? && chain_matched_stack.last
|
|
49
|
+
# Previous branch matched, so this one is false
|
|
50
|
+
condition_stack[-1] = false
|
|
51
|
+
else
|
|
52
|
+
# Previous branch didn't match, evaluate this condition
|
|
53
|
+
result = ConditionEvaluator.evaluate(condition, context)
|
|
54
|
+
condition_stack[-1] = result
|
|
55
|
+
chain_matched_stack[-1] = result if result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Don't include the @elsif line itself
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check for @else
|
|
63
|
+
if line =~ /^@else\s*$/i
|
|
64
|
+
# If any previous condition in chain was true, this branch is false
|
|
65
|
+
# Otherwise, this branch is true
|
|
66
|
+
if !condition_stack.empty? && chain_matched_stack.last
|
|
67
|
+
# Previous branch matched, so else is false
|
|
68
|
+
condition_stack[-1] = false
|
|
69
|
+
else
|
|
70
|
+
# No previous branch matched, so else is true
|
|
71
|
+
condition_stack[-1] = true
|
|
72
|
+
chain_matched_stack[-1] = true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Don't include the @else line itself
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check for @end - only skip if it's closing an @if/@unless/@elsif/@else block
|
|
80
|
+
if (line =~ /^@end\s*$/) && !condition_stack.empty?
|
|
81
|
+
# This @end closes a conditional block, so skip it
|
|
82
|
+
condition_stack.pop
|
|
83
|
+
chain_matched_stack.pop
|
|
84
|
+
next
|
|
85
|
+
end
|
|
86
|
+
# Otherwise, this @end is for @before/@after, so include it
|
|
87
|
+
|
|
88
|
+
# Include the line only if all conditions in stack are true
|
|
89
|
+
output << line if condition_stack.all? { |cond| cond }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
output.join("\n")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/howzit/config.rb
CHANGED
|
@@ -129,14 +129,26 @@ module Howzit
|
|
|
129
129
|
|
|
130
130
|
## Update editor config
|
|
131
131
|
def update_editor
|
|
132
|
-
|
|
132
|
+
begin
|
|
133
|
+
puts 'No $EDITOR defined, no value in config'
|
|
134
|
+
rescue Errno::EPIPE
|
|
135
|
+
# Pipe closed, ignore
|
|
136
|
+
end
|
|
133
137
|
editor = Prompt.read_editor
|
|
134
138
|
if editor.nil?
|
|
135
|
-
|
|
139
|
+
begin
|
|
140
|
+
puts 'Cancelled, no editor stored.'
|
|
141
|
+
rescue Errno::EPIPE
|
|
142
|
+
# Pipe closed, ignore
|
|
143
|
+
end
|
|
136
144
|
Process.exit 1
|
|
137
145
|
end
|
|
138
146
|
update_config_option({ config_editor: editor, editor: editor })
|
|
139
|
-
|
|
147
|
+
begin
|
|
148
|
+
puts "Default editor set to #{editor}, modify in config file"
|
|
149
|
+
rescue Errno::EPIPE
|
|
150
|
+
# Pipe closed, ignore
|
|
151
|
+
end
|
|
140
152
|
editor
|
|
141
153
|
end
|
|
142
154
|
|
|
@@ -39,7 +39,13 @@ module Howzit
|
|
|
39
39
|
## @param level [Symbol] The level
|
|
40
40
|
##
|
|
41
41
|
def write(msg, level = :info)
|
|
42
|
-
|
|
42
|
+
return unless LOG_LEVELS[level] >= @log_level
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
$stderr.puts msg
|
|
46
|
+
rescue Errno::EPIPE
|
|
47
|
+
# Pipe closed, ignore
|
|
48
|
+
end
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
##
|
|
@@ -66,7 +72,13 @@ module Howzit
|
|
|
66
72
|
## @param msg The message
|
|
67
73
|
##
|
|
68
74
|
def warn(msg)
|
|
69
|
-
|
|
75
|
+
return unless LOG_LEVELS[:warn] >= @log_level
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
$stderr.puts msg
|
|
79
|
+
rescue Errno::EPIPE
|
|
80
|
+
# Pipe closed, ignore
|
|
81
|
+
end
|
|
70
82
|
end
|
|
71
83
|
|
|
72
84
|
##
|
data/lib/howzit/prompt.rb
CHANGED
|
@@ -100,9 +100,7 @@ module Howzit
|
|
|
100
100
|
return fzf_result(res)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
if Util.command_exist?('gum')
|
|
104
|
-
return gum_choose(matches, query: query, multi: true)
|
|
105
|
-
end
|
|
103
|
+
return gum_choose(matches, query: query, multi: true) if Util.command_exist?('gum')
|
|
106
104
|
|
|
107
105
|
tty_menu(matches, query: query)
|
|
108
106
|
end
|
|
@@ -150,7 +148,11 @@ module Howzit
|
|
|
150
148
|
end
|
|
151
149
|
|
|
152
150
|
if query
|
|
153
|
-
|
|
151
|
+
begin
|
|
152
|
+
puts "\nSelect a topic for `#{query}`:"
|
|
153
|
+
rescue Errno::EPIPE
|
|
154
|
+
# Pipe closed, ignore
|
|
155
|
+
end
|
|
154
156
|
end
|
|
155
157
|
options_list(matches)
|
|
156
158
|
read_selection(matches)
|
|
@@ -168,7 +170,11 @@ module Howzit
|
|
|
168
170
|
|
|
169
171
|
return [matches[line - 1]] if line.positive? && line <= matches.length
|
|
170
172
|
|
|
171
|
-
|
|
173
|
+
begin
|
|
174
|
+
puts 'Out of range'
|
|
175
|
+
rescue Errno::EPIPE
|
|
176
|
+
# Pipe closed, ignore
|
|
177
|
+
end
|
|
172
178
|
read_selection(matches)
|
|
173
179
|
end
|
|
174
180
|
ensure
|
|
@@ -229,9 +235,7 @@ module Howzit
|
|
|
229
235
|
return res.empty? ? [] : res.split(/\n/)
|
|
230
236
|
end
|
|
231
237
|
|
|
232
|
-
if Util.command_exist?('gum')
|
|
233
|
-
return gum_choose(matches, prompt: prompt_text, multi: true, required: false)
|
|
234
|
-
end
|
|
238
|
+
return gum_choose(matches, prompt: prompt_text, multi: true, required: false) if Util.command_exist?('gum')
|
|
235
239
|
|
|
236
240
|
text_template_input(matches)
|
|
237
241
|
end
|
|
@@ -265,7 +269,11 @@ module Howzit
|
|
|
265
269
|
exit
|
|
266
270
|
end
|
|
267
271
|
|
|
268
|
-
|
|
272
|
+
begin
|
|
273
|
+
puts "\n{bw}Available templates:{x} #{available.join(', ')}".c
|
|
274
|
+
rescue Errno::EPIPE
|
|
275
|
+
# Pipe closed, ignore
|
|
276
|
+
end
|
|
269
277
|
printf '{bw}Enter templates to include, comma-separated (return to skip):{x} '.c
|
|
270
278
|
input = Readline.readline('', true).strip
|
|
271
279
|
|
|
@@ -321,7 +329,7 @@ module Howzit
|
|
|
321
329
|
## @return [String] the entered value
|
|
322
330
|
##
|
|
323
331
|
def get_line(prompt_text, default: nil)
|
|
324
|
-
return
|
|
332
|
+
return default || '' unless $stdout.isatty
|
|
325
333
|
|
|
326
334
|
if Util.command_exist?('gum')
|
|
327
335
|
result = gum_input(prompt_text, placeholder: default || '')
|
|
@@ -346,7 +354,7 @@ module Howzit
|
|
|
346
354
|
##
|
|
347
355
|
def gum_choose(matches, prompt: nil, multi: false, required: true, query: nil)
|
|
348
356
|
prompt_text = prompt || (query ? "Select for '#{query}'" : 'Select')
|
|
349
|
-
args = [
|
|
357
|
+
args = %w[gum choose]
|
|
350
358
|
args << '--no-limit' if multi
|
|
351
359
|
args << "--header=#{Shellwords.escape(prompt_text)}"
|
|
352
360
|
args << '--cursor.foreground=6'
|
|
@@ -376,7 +384,7 @@ module Howzit
|
|
|
376
384
|
## @return [String] The entered value
|
|
377
385
|
##
|
|
378
386
|
def gum_input(prompt_text, placeholder: '')
|
|
379
|
-
args = [
|
|
387
|
+
args = %w[gum input]
|
|
380
388
|
args << "--header=#{Shellwords.escape(prompt_text)}"
|
|
381
389
|
args << "--placeholder=#{Shellwords.escape(placeholder)}" unless placeholder.empty?
|
|
382
390
|
args << '--cursor.foreground=6'
|
data/lib/howzit/run_report.rb
CHANGED
|
@@ -57,7 +57,7 @@ module Howzit
|
|
|
57
57
|
|
|
58
58
|
# Build the table with emoji header - center emoji in 6-char column
|
|
59
59
|
header = "| 🚥 | #{'Task'.ljust(task_width)} |"
|
|
60
|
-
separator = "| :--: | #{'
|
|
60
|
+
separator = "| :--: | #{":#{'-' * (task_width - 1)}"} |"
|
|
61
61
|
|
|
62
62
|
table_lines = [header, separator]
|
|
63
63
|
rows.each do |row|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Howzit
|
|
4
|
+
# Script Communication module
|
|
5
|
+
# Handles communication from scripts to Howzit via a communication file
|
|
6
|
+
module ScriptComm
|
|
7
|
+
class << self
|
|
8
|
+
##
|
|
9
|
+
## Create a communication file for scripts to write to
|
|
10
|
+
##
|
|
11
|
+
## @return [String] Path to the communication file
|
|
12
|
+
##
|
|
13
|
+
def create_comm_file
|
|
14
|
+
file = Tempfile.new('howzit_comm')
|
|
15
|
+
file.close
|
|
16
|
+
file.path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
## Set up the communication file and environment variable
|
|
21
|
+
##
|
|
22
|
+
## @return [String] Path to the communication file
|
|
23
|
+
##
|
|
24
|
+
def setup
|
|
25
|
+
comm_file = create_comm_file
|
|
26
|
+
ENV['HOWZIT_COMM_FILE'] = comm_file
|
|
27
|
+
comm_file
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
## Read and process the communication file after script execution
|
|
32
|
+
##
|
|
33
|
+
## @param comm_file [String] Path to the communication file
|
|
34
|
+
##
|
|
35
|
+
## @return [Hash] Hash with :logs and :vars keys
|
|
36
|
+
##
|
|
37
|
+
def process(comm_file)
|
|
38
|
+
return { logs: [], vars: {} } unless File.exist?(comm_file)
|
|
39
|
+
|
|
40
|
+
logs = []
|
|
41
|
+
vars = {}
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
content = File.read(comm_file)
|
|
45
|
+
content.each_line do |line|
|
|
46
|
+
line = line.strip
|
|
47
|
+
next if line.empty?
|
|
48
|
+
|
|
49
|
+
case line
|
|
50
|
+
when /^LOG:(info|warn|error|debug):(.+)$/i
|
|
51
|
+
level = Regexp.last_match(1).downcase.to_sym
|
|
52
|
+
message = Regexp.last_match(2)
|
|
53
|
+
logs << { level: level, message: message }
|
|
54
|
+
when /^VAR:([A-Z0-9_]+)=(.*)$/i
|
|
55
|
+
key = Regexp.last_match(1)
|
|
56
|
+
value = Regexp.last_match(2)
|
|
57
|
+
vars[key] = value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
Howzit.console&.warn("Error reading communication file: #{e.message}")
|
|
62
|
+
ensure
|
|
63
|
+
# Clean up the file
|
|
64
|
+
File.unlink(comm_file) if File.exist?(comm_file)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
{ logs: logs, vars: vars }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
## Process communication and apply logs/variables
|
|
72
|
+
##
|
|
73
|
+
## @param comm_file [String] Path to the communication file
|
|
74
|
+
##
|
|
75
|
+
def apply(comm_file)
|
|
76
|
+
result = process(comm_file)
|
|
77
|
+
return if result[:logs].empty? && result[:vars].empty?
|
|
78
|
+
|
|
79
|
+
# Apply log messages
|
|
80
|
+
result[:logs].each do |log_entry|
|
|
81
|
+
level = log_entry[:level]
|
|
82
|
+
message = log_entry[:message]
|
|
83
|
+
next unless Howzit.console
|
|
84
|
+
|
|
85
|
+
case level
|
|
86
|
+
when :info
|
|
87
|
+
Howzit.console.info(message)
|
|
88
|
+
when :warn
|
|
89
|
+
Howzit.console.warn(message)
|
|
90
|
+
when :error
|
|
91
|
+
Howzit.console.error(message)
|
|
92
|
+
when :debug
|
|
93
|
+
Howzit.console.debug(message)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Apply variables to named_arguments
|
|
98
|
+
return if result[:vars].empty?
|
|
99
|
+
|
|
100
|
+
Howzit.named_arguments ||= {}
|
|
101
|
+
Howzit.named_arguments.merge!(result[:vars])
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/howzit/stringutils.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Howzit
|
|
|
38
38
|
position = 0
|
|
39
39
|
in_order = 0
|
|
40
40
|
chars.each do |char|
|
|
41
|
-
new_pos = self[position
|
|
41
|
+
new_pos = self[position..] =~ /#{char}/i
|
|
42
42
|
if new_pos
|
|
43
43
|
position += new_pos
|
|
44
44
|
in_order += 1
|
|
@@ -72,7 +72,7 @@ module Howzit
|
|
|
72
72
|
##
|
|
73
73
|
def distance(chars)
|
|
74
74
|
distance = 0
|
|
75
|
-
max =
|
|
75
|
+
max = length - chars.length
|
|
76
76
|
return max unless in_order(chars) == chars.length
|
|
77
77
|
|
|
78
78
|
while distance < max
|
|
@@ -219,7 +219,7 @@ module Howzit
|
|
|
219
219
|
def uncolor
|
|
220
220
|
# force UTF-8 and remove invalid characters, then remove color codes
|
|
221
221
|
# and iTerm markers
|
|
222
|
-
gsub(Howzit::Color::COLORED_REGEXP,
|
|
222
|
+
gsub(Howzit::Color::COLORED_REGEXP, '').gsub(/\e\]1337;SetMark/, '')
|
|
223
223
|
end
|
|
224
224
|
|
|
225
225
|
# Wrap text at a specified width.
|
|
@@ -371,7 +371,7 @@ module Howzit
|
|
|
371
371
|
gsub!(/\$\{(?<name>[A-Z0-9_]+(?::.*?)?)\}/i) do
|
|
372
372
|
m = Regexp.last_match
|
|
373
373
|
arg, default = m['name'].split(/:/).map(&:strip)
|
|
374
|
-
if Howzit.named_arguments
|
|
374
|
+
if Howzit.named_arguments&.key?(arg) && !Howzit.named_arguments[arg].nil?
|
|
375
375
|
Howzit.named_arguments[arg]
|
|
376
376
|
elsif default
|
|
377
377
|
default
|