howzit 2.1.27 → 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 +71 -0
- data/Rakefile +4 -3
- data/bin/howzit +65 -65
- data/howzit.gemspec +1 -1
- data/lib/howzit/buildnote.rb +115 -45
- 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 +40 -6
- data/lib/howzit/task.rb +11 -2
- data/lib/howzit/topic.rb +31 -10
- data/lib/howzit/util.rb +11 -3
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +4 -1
- 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,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
|
|
@@ -175,9 +175,43 @@ module Howzit
|
|
|
175
175
|
when 'beginswith'
|
|
176
176
|
/^#{self}/i
|
|
177
177
|
when 'fuzzy'
|
|
178
|
-
|
|
178
|
+
# For fuzzy matching, use token-based matching where each word gets fuzzy character matching
|
|
179
|
+
# This allows "lst tst" to match "List Available Tests" by applying fuzzy matching to each token
|
|
180
|
+
words = split(/\s+/).reject(&:empty?)
|
|
181
|
+
if words.length > 1
|
|
182
|
+
# Multiple words: apply character-by-character fuzzy matching to each word token
|
|
183
|
+
# Then allow flexible matching between words
|
|
184
|
+
pattern = words.map do |w|
|
|
185
|
+
# Apply fuzzy matching to each word (character-by-character with up to 3 chars between)
|
|
186
|
+
w.split(//).map { |c| Regexp.escape(c) }.join('.{0,3}?')
|
|
187
|
+
end.join('.*')
|
|
188
|
+
/#{pattern}/i
|
|
189
|
+
else
|
|
190
|
+
# Single word: character-by-character fuzzy matching for flexibility
|
|
191
|
+
/#{split(//).join('.{0,3}?')}/i
|
|
192
|
+
end
|
|
193
|
+
when 'token'
|
|
194
|
+
# Token-based matching: match words in order with any text between them
|
|
195
|
+
# "list tests" matches "list available tests", "list of tests", etc.
|
|
196
|
+
words = split(/\s+/).reject(&:empty?)
|
|
197
|
+
if words.length > 1
|
|
198
|
+
pattern = words.map { |w| Regexp.escape(w) }.join('.*')
|
|
199
|
+
/#{pattern}/i
|
|
200
|
+
else
|
|
201
|
+
/#{Regexp.escape(self)}/i
|
|
202
|
+
end
|
|
179
203
|
else
|
|
180
|
-
|
|
204
|
+
# Default 'partial' mode: token-based matching for multi-word searches
|
|
205
|
+
# This allows "list tests" to match "list available tests"
|
|
206
|
+
words = split(/\s+/).reject(&:empty?)
|
|
207
|
+
if words.length > 1
|
|
208
|
+
# Token-based: match words in order with any text between
|
|
209
|
+
pattern = words.map { |w| Regexp.escape(w) }.join('.*')
|
|
210
|
+
/#{pattern}/i
|
|
211
|
+
else
|
|
212
|
+
# Single word: simple substring match
|
|
213
|
+
/#{Regexp.escape(self)}/i
|
|
214
|
+
end
|
|
181
215
|
end
|
|
182
216
|
end
|
|
183
217
|
|
|
@@ -185,7 +219,7 @@ module Howzit
|
|
|
185
219
|
def uncolor
|
|
186
220
|
# force UTF-8 and remove invalid characters, then remove color codes
|
|
187
221
|
# and iTerm markers
|
|
188
|
-
gsub(Howzit::Color::COLORED_REGEXP,
|
|
222
|
+
gsub(Howzit::Color::COLORED_REGEXP, '').gsub(/\e\]1337;SetMark/, '')
|
|
189
223
|
end
|
|
190
224
|
|
|
191
225
|
# Wrap text at a specified width.
|
|
@@ -337,7 +371,7 @@ module Howzit
|
|
|
337
371
|
gsub!(/\$\{(?<name>[A-Z0-9_]+(?::.*?)?)\}/i) do
|
|
338
372
|
m = Regexp.last_match
|
|
339
373
|
arg, default = m['name'].split(/:/).map(&:strip)
|
|
340
|
-
if Howzit.named_arguments
|
|
374
|
+
if Howzit.named_arguments&.key?(arg) && !Howzit.named_arguments[arg].nil?
|
|
341
375
|
Howzit.named_arguments[arg]
|
|
342
376
|
elsif default
|
|
343
377
|
default
|
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]
|
|
29
|
+
@title = attributes[:title]&.to_s
|
|
30
30
|
@parent = attributes[:parent] || nil
|
|
31
31
|
|
|
32
32
|
@action = attributes[:action].render_arguments || nil
|
|
@@ -61,6 +61,7 @@ module Howzit
|
|
|
61
61
|
Howzit.console.info "#{@prefix}{bg}Running block {bw}#{@title}{x}".c if Howzit.options[:log_level] < 2
|
|
62
62
|
block = @action
|
|
63
63
|
script = Tempfile.new('howzit_script')
|
|
64
|
+
comm_file = ScriptComm.setup
|
|
64
65
|
begin
|
|
65
66
|
script.write(block)
|
|
66
67
|
script.close
|
|
@@ -69,6 +70,8 @@ module Howzit
|
|
|
69
70
|
ensure
|
|
70
71
|
script.close
|
|
71
72
|
script.unlink
|
|
73
|
+
# Process script communication
|
|
74
|
+
ScriptComm.apply(comm_file) if comm_file
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
update_last_status(res ? 0 : 1)
|
|
@@ -112,7 +115,13 @@ module Howzit
|
|
|
112
115
|
end
|
|
113
116
|
Howzit.console.info("#{@prefix}{bg}Running {bw}#{display_title}{x}".c)
|
|
114
117
|
ENV['HOWZIT_SCRIPTS'] = File.expand_path('~/.config/howzit/scripts')
|
|
115
|
-
|
|
118
|
+
comm_file = ScriptComm.setup
|
|
119
|
+
begin
|
|
120
|
+
res = system(@action)
|
|
121
|
+
ensure
|
|
122
|
+
# Process script communication
|
|
123
|
+
ScriptComm.apply(comm_file) if comm_file
|
|
124
|
+
end
|
|
116
125
|
update_last_status(res ? 0 : 1)
|
|
117
126
|
res
|
|
118
127
|
end
|
data/lib/howzit/topic.rb
CHANGED
|
@@ -14,13 +14,15 @@ module Howzit
|
|
|
14
14
|
##
|
|
15
15
|
## @param title [String] The topic title
|
|
16
16
|
## @param content [String] The raw topic content
|
|
17
|
+
## @param metadata [Hash] Optional metadata hash
|
|
17
18
|
##
|
|
18
|
-
def initialize(title, content)
|
|
19
|
+
def initialize(title, content, metadata = nil)
|
|
19
20
|
@title = title
|
|
20
21
|
@content = content
|
|
21
22
|
@parent = nil
|
|
22
23
|
@nest_level = 0
|
|
23
24
|
@named_args = {}
|
|
25
|
+
@metadata = metadata
|
|
24
26
|
arguments
|
|
25
27
|
|
|
26
28
|
@tasks = gather_tasks
|
|
@@ -37,7 +39,7 @@ module Howzit
|
|
|
37
39
|
args.each_with_index do |arg, idx|
|
|
38
40
|
arg_name, default = arg.split(/:/).map(&:strip)
|
|
39
41
|
|
|
40
|
-
@named_args[arg_name] = if Howzit.arguments.count >= idx + 1
|
|
42
|
+
@named_args[arg_name] = if Howzit.arguments && Howzit.arguments.count >= idx + 1
|
|
41
43
|
Howzit.arguments[idx]
|
|
42
44
|
else
|
|
43
45
|
default
|
|
@@ -81,7 +83,11 @@ module Howzit
|
|
|
81
83
|
|
|
82
84
|
if @tasks.count.positive?
|
|
83
85
|
unless @prereqs.empty?
|
|
84
|
-
|
|
86
|
+
begin
|
|
87
|
+
puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
88
|
+
rescue Errno::EPIPE
|
|
89
|
+
# Pipe closed, ignore
|
|
90
|
+
end
|
|
85
91
|
res = Prompt.yn('Have the above prerequisites been met?', default: true)
|
|
86
92
|
Process.exit 1 unless res
|
|
87
93
|
|
|
@@ -123,7 +129,13 @@ module Howzit
|
|
|
123
129
|
|
|
124
130
|
output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
|
|
125
131
|
|
|
126
|
-
|
|
132
|
+
unless @postreqs.empty?
|
|
133
|
+
begin
|
|
134
|
+
puts TTY::Box.frame("{bw}#{@postreqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
135
|
+
rescue Errno::EPIPE
|
|
136
|
+
# Pipe closed, ignore
|
|
137
|
+
end
|
|
138
|
+
end
|
|
127
139
|
|
|
128
140
|
output
|
|
129
141
|
end
|
|
@@ -175,6 +187,7 @@ module Howzit
|
|
|
175
187
|
return [] if matches.empty?
|
|
176
188
|
|
|
177
189
|
topic = matches[0]
|
|
190
|
+
return [] if topic.nil?
|
|
178
191
|
|
|
179
192
|
rule = '{kKd}'
|
|
180
193
|
color = '{Kyd}'
|
|
@@ -253,14 +266,16 @@ module Howzit
|
|
|
253
266
|
output.push(@title.format_header)
|
|
254
267
|
output.push('')
|
|
255
268
|
end
|
|
256
|
-
|
|
269
|
+
# Process conditional blocks first
|
|
270
|
+
metadata = @metadata || Howzit.buildnote&.metadata
|
|
271
|
+
topic = ConditionalContent.process(@content.dup, { metadata: metadata })
|
|
257
272
|
unless Howzit.options[:show_all_code]
|
|
258
273
|
topic.gsub!(/(?mix)^(`{3,})run([?!]*)\s*
|
|
259
274
|
([^\n]*)[\s\S]*?\n\1\s*$/, '@@@run\2 \3')
|
|
260
275
|
end
|
|
261
276
|
topic.split(/\n/).each do |l|
|
|
262
277
|
case l
|
|
263
|
-
when /@(before|after|prereq|end)/
|
|
278
|
+
when /@(before|after|prereq|end|if|unless)/
|
|
264
279
|
next
|
|
265
280
|
when /@include(?<optional>[!?]{1,2})?\((?<action>[^)]+)\)/
|
|
266
281
|
output.concat(process_include(Regexp.last_match.named_captures.symbolize_keys, opt))
|
|
@@ -299,7 +314,7 @@ module Howzit
|
|
|
299
314
|
# Store the actual title (not overridden by show_all_code - that's only for display)
|
|
300
315
|
task_args = { type: :include,
|
|
301
316
|
arguments: nil,
|
|
302
|
-
title: title.dup,
|
|
317
|
+
title: title.dup, # Make a copy to avoid reference issues
|
|
303
318
|
action: obj,
|
|
304
319
|
parent: self }
|
|
305
320
|
# Set named_arguments before processing titles for variable substitution
|
|
@@ -362,15 +377,21 @@ module Howzit
|
|
|
362
377
|
|
|
363
378
|
def gather_tasks
|
|
364
379
|
runnable = []
|
|
365
|
-
|
|
366
|
-
|
|
380
|
+
# Process conditional blocks first
|
|
381
|
+
# Set named_arguments before processing so conditions can access them
|
|
382
|
+
Howzit.named_arguments = @named_args
|
|
383
|
+
metadata = @metadata || Howzit.buildnote&.metadata
|
|
384
|
+
processed_content = ConditionalContent.process(@content, { metadata: metadata })
|
|
385
|
+
|
|
386
|
+
@prereqs = processed_content.scan(/(?<=@before\n).*?(?=\n@end)/im).map(&:strip)
|
|
387
|
+
@postreqs = processed_content.scan(/(?<=@after\n).*?(?=\n@end)/im).map(&:strip)
|
|
367
388
|
|
|
368
389
|
rx = /(?mix)(?:
|
|
369
390
|
@(?<cmd>include|run|copy|open|url)(?<optional>[!?]{1,2})?\((?<action>[^)]*?)\)(?<title>[^\n]+)?
|
|
370
391
|
|(?<fence>`{3,})run(?<optional2>[!?]{1,2})?(?<title2>[^\n]+)?(?<block>.*?)\k<fence>
|
|
371
392
|
)/
|
|
372
393
|
matches = []
|
|
373
|
-
|
|
394
|
+
processed_content.scan(rx) { matches << Regexp.last_match }
|
|
374
395
|
matches.each do |m|
|
|
375
396
|
c = m.named_captures.symbolize_keys
|
|
376
397
|
Howzit.named_arguments = @named_args
|
data/lib/howzit/util.rb
CHANGED
|
@@ -118,7 +118,11 @@ module Howzit
|
|
|
118
118
|
# Paginate the output
|
|
119
119
|
def page(text)
|
|
120
120
|
unless $stdout.isatty
|
|
121
|
-
|
|
121
|
+
begin
|
|
122
|
+
puts text
|
|
123
|
+
rescue Errno::EPIPE
|
|
124
|
+
# Pipe closed, ignore
|
|
125
|
+
end
|
|
122
126
|
return
|
|
123
127
|
end
|
|
124
128
|
|
|
@@ -181,7 +185,11 @@ module Howzit
|
|
|
181
185
|
if options[:paginate] && Howzit.options[:paginate]
|
|
182
186
|
page(output)
|
|
183
187
|
else
|
|
184
|
-
|
|
188
|
+
begin
|
|
189
|
+
puts output
|
|
190
|
+
rescue Errno::EPIPE
|
|
191
|
+
# Pipe closed, ignore
|
|
192
|
+
end
|
|
185
193
|
end
|
|
186
194
|
end
|
|
187
195
|
|
|
@@ -219,7 +227,7 @@ module Howzit
|
|
|
219
227
|
##
|
|
220
228
|
def os_paste
|
|
221
229
|
os = RbConfig::CONFIG['target_os']
|
|
222
|
-
out =
|
|
230
|
+
out = '{bg}Pasting from clipboard'.c
|
|
223
231
|
case os
|
|
224
232
|
when /darwin.*/i
|
|
225
233
|
Howzit.console.debug("#{out} (macOS){x}".c)
|
data/lib/howzit/version.rb
CHANGED
data/lib/howzit.rb
CHANGED
|
@@ -13,7 +13,7 @@ COLOR_FILE = 'theme.yaml'
|
|
|
13
13
|
IGNORE_FILE = 'ignore.yaml'
|
|
14
14
|
|
|
15
15
|
# Available options for matching method
|
|
16
|
-
MATCHING_OPTIONS = %w[partial exact fuzzy beginswith].freeze
|
|
16
|
+
MATCHING_OPTIONS = %w[partial exact fuzzy beginswith token].freeze
|
|
17
17
|
|
|
18
18
|
# Available options for multiple_matches method
|
|
19
19
|
MULTIPLE_OPTIONS = %w[first best all choose].freeze
|
|
@@ -38,6 +38,9 @@ require_relative 'howzit/stringutils'
|
|
|
38
38
|
|
|
39
39
|
require_relative 'howzit/console_logger'
|
|
40
40
|
require_relative 'howzit/config'
|
|
41
|
+
require_relative 'howzit/script_comm'
|
|
42
|
+
require_relative 'howzit/condition_evaluator'
|
|
43
|
+
require_relative 'howzit/conditional_content'
|
|
41
44
|
require_relative 'howzit/task'
|
|
42
45
|
require_relative 'howzit/topic'
|
|
43
46
|
require_relative 'howzit/buildnote'
|