howzit 2.1.29 → 2.1.31
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 +95 -0
- data/README.md +5 -1
- data/Rakefile +7 -1
- data/bin/howzit +5 -0
- data/lib/howzit/buildnote.rb +23 -1
- data/lib/howzit/config.rb +21 -0
- data/lib/howzit/console_logger.rb +62 -2
- data/lib/howzit/directive.rb +137 -0
- data/lib/howzit/script_support.rb +572 -0
- data/lib/howzit/stringutils.rb +47 -5
- data/lib/howzit/task.rb +57 -4
- data/lib/howzit/topic.rb +548 -5
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +8 -1
- data/spec/buildnote_spec.rb +33 -4
- data/spec/log_level_spec.rb +247 -0
- data/spec/sequential_conditional_spec.rb +319 -0
- data/spec/set_var_spec.rb +603 -0
- data/spec/stringutils_spec.rb +41 -1
- data/spec/topic_spec.rb +8 -6
- data/src/_README.md +5 -1
- metadata +10 -2
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module Howzit
|
|
7
|
+
# Script Support module
|
|
8
|
+
# Handles helper script installation and injection for run blocks
|
|
9
|
+
# rubocop:disable Metrics/ModuleLength
|
|
10
|
+
module ScriptSupport
|
|
11
|
+
# Prefer XDG_CONFIG_HOME/howzit/support if set, otherwise ~/.config/howzit/support.
|
|
12
|
+
# For backwards compatibility, we detect an existing ~/.local/share/howzit directory
|
|
13
|
+
# and can optionally migrate it to the new location.
|
|
14
|
+
LEGACY_SUPPORT_DIR = File.join(Dir.home, '.local', 'share', 'howzit')
|
|
15
|
+
SUPPORT_DIR = if ENV['XDG_CONFIG_HOME'] && !ENV['XDG_CONFIG_HOME'].empty?
|
|
16
|
+
File.join(ENV['XDG_CONFIG_HOME'], 'howzit', 'support')
|
|
17
|
+
else
|
|
18
|
+
File.join(Dir.home, '.config', 'howzit', 'support')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
##
|
|
23
|
+
## Get the support directory path
|
|
24
|
+
##
|
|
25
|
+
## @return [String] expanded path to support directory
|
|
26
|
+
##
|
|
27
|
+
def support_dir
|
|
28
|
+
File.expand_path(SUPPORT_DIR)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
## Ensure support directory exists and is populated
|
|
33
|
+
##
|
|
34
|
+
def ensure_support_dir
|
|
35
|
+
dir = support_dir
|
|
36
|
+
legacy_dir = File.expand_path(LEGACY_SUPPORT_DIR)
|
|
37
|
+
new_root = File.expand_path(File.join(SUPPORT_DIR, '..'))
|
|
38
|
+
|
|
39
|
+
# If legacy files exist, always offer to migrate them before proceeding.
|
|
40
|
+
# Use early_init=false here since config is already loaded by the time we reach this point
|
|
41
|
+
migrate_legacy_support(early_init: false) if File.directory?(legacy_dir)
|
|
42
|
+
|
|
43
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
44
|
+
install_helper_scripts
|
|
45
|
+
dir
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
## Simple Y/N prompt that doesn't depend on Howzit.options (for use during early config initialization)
|
|
50
|
+
##
|
|
51
|
+
def simple_yn_prompt(prompt, default: true)
|
|
52
|
+
return default unless $stdout.isatty
|
|
53
|
+
|
|
54
|
+
tty_state = `stty -g`
|
|
55
|
+
yn = default ? '[Y/n]' : '[y/N]'
|
|
56
|
+
$stdout.syswrite "\e[1;37m#{prompt} #{yn}\e[1;37m? \e[0m"
|
|
57
|
+
system 'stty raw -echo cbreak isig'
|
|
58
|
+
res = $stdin.sysread 1
|
|
59
|
+
res.chomp!
|
|
60
|
+
puts
|
|
61
|
+
system 'stty cooked'
|
|
62
|
+
system "stty #{tty_state}"
|
|
63
|
+
res.empty? ? default : res =~ /y/i
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
## Migrate legacy ~/.local/share/howzit directory into the new config root,
|
|
68
|
+
## merging contents and removing the legacy directory when complete.
|
|
69
|
+
##
|
|
70
|
+
## @param early_init [Boolean] If true, use simple prompt that doesn't access Howzit.options
|
|
71
|
+
##
|
|
72
|
+
def migrate_legacy_support(early_init: false)
|
|
73
|
+
legacy_dir = File.expand_path(LEGACY_SUPPORT_DIR)
|
|
74
|
+
new_root = File.expand_path(File.join(SUPPORT_DIR, '..'))
|
|
75
|
+
|
|
76
|
+
unless File.directory?(legacy_dir)
|
|
77
|
+
if early_init
|
|
78
|
+
return
|
|
79
|
+
else
|
|
80
|
+
Howzit.console.info "No legacy Howzit directory found at #{legacy_dir}; nothing to migrate."
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
prompt = "Migrate Howzit files from #{legacy_dir} to #{new_root}? This will overwrite files in the new location with legacy versions, " \
|
|
86
|
+
'add new files, and leave any extra files in the new location in place.'
|
|
87
|
+
|
|
88
|
+
if early_init
|
|
89
|
+
unless simple_yn_prompt(prompt, default: true)
|
|
90
|
+
$stderr.puts 'Migration cancelled; no changes made.'
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
unless Prompt.yn(prompt, default: true)
|
|
95
|
+
Howzit.console.info 'Migration cancelled; no changes made.'
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
FileUtils.mkdir_p(new_root) unless File.directory?(new_root)
|
|
101
|
+
|
|
102
|
+
# Merge legacy into new_root:
|
|
103
|
+
# - overwrite files in new_root with versions from legacy
|
|
104
|
+
# - add files that do not yet exist
|
|
105
|
+
# - leave files that exist only in new_root untouched
|
|
106
|
+
Dir.children(legacy_dir).each do |entry|
|
|
107
|
+
src = File.join(legacy_dir, entry)
|
|
108
|
+
dest = File.join(new_root, entry)
|
|
109
|
+
|
|
110
|
+
if File.directory?(src)
|
|
111
|
+
FileUtils.mkdir_p(dest)
|
|
112
|
+
FileUtils.cp_r(File.join(src, '.'), dest)
|
|
113
|
+
else
|
|
114
|
+
FileUtils.cp(src, dest)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
FileUtils.rm_rf(legacy_dir)
|
|
119
|
+
if early_init
|
|
120
|
+
$stderr.puts "Migrated Howzit files from #{legacy_dir} to #{new_root}."
|
|
121
|
+
else
|
|
122
|
+
Howzit.console.info "Migrated Howzit files from #{legacy_dir} to #{new_root}."
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
##
|
|
127
|
+
## Detect interpreter from hashbang line
|
|
128
|
+
##
|
|
129
|
+
## @param script_content [String] The script content
|
|
130
|
+
##
|
|
131
|
+
## @return [Symbol, nil] Language identifier (:bash, :zsh, :fish, :ruby, :python, etc.)
|
|
132
|
+
##
|
|
133
|
+
def detect_interpreter(script_content)
|
|
134
|
+
first_line = script_content.lines.first&.strip
|
|
135
|
+
return nil unless first_line&.start_with?('#!')
|
|
136
|
+
|
|
137
|
+
shebang = first_line.sub(/^#!/, '').strip
|
|
138
|
+
|
|
139
|
+
case shebang
|
|
140
|
+
when %r{/bin/bash}, %r{/usr/bin/env bash}
|
|
141
|
+
:bash
|
|
142
|
+
when %r{/bin/zsh}, %r{/usr/bin/env zsh}
|
|
143
|
+
:zsh
|
|
144
|
+
when %r{/bin/fish}, %r{/usr/bin/env fish}
|
|
145
|
+
:fish
|
|
146
|
+
when %r{/usr/bin/env ruby}, %r{/usr/bin/ruby}, %r{/usr/local/bin/ruby}
|
|
147
|
+
:ruby
|
|
148
|
+
when %r{/usr/bin/env python3?}, %r{/usr/bin/python3?}, %r{/usr/local/bin/python3?}
|
|
149
|
+
:python
|
|
150
|
+
when %r{/usr/bin/env perl}, %r{/usr/bin/perl}
|
|
151
|
+
:perl
|
|
152
|
+
when %r{/usr/bin/env node}, %r{/usr/bin/node}
|
|
153
|
+
:node
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
##
|
|
158
|
+
## Get the injection line for a given interpreter
|
|
159
|
+
##
|
|
160
|
+
## @param interpreter [Symbol] The interpreter type
|
|
161
|
+
##
|
|
162
|
+
## @return [String, nil] The injection line to add
|
|
163
|
+
##
|
|
164
|
+
def injection_line_for(interpreter)
|
|
165
|
+
support_path = support_dir
|
|
166
|
+
case interpreter
|
|
167
|
+
when :bash, :zsh
|
|
168
|
+
"source \"#{support_path}/howzit.sh\""
|
|
169
|
+
when :fish
|
|
170
|
+
"source \"#{support_path}/howzit.fish\""
|
|
171
|
+
when :ruby
|
|
172
|
+
"require '#{support_path}/howzit.rb'"
|
|
173
|
+
when :python
|
|
174
|
+
"import sys\nsys.path.insert(0, '#{support_path}')\nimport howzit"
|
|
175
|
+
when :perl
|
|
176
|
+
"require '#{support_path}/howzit.pl'"
|
|
177
|
+
when :node
|
|
178
|
+
"require('#{support_path}/howzit.js')"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
##
|
|
183
|
+
## Inject helper script loading into script content
|
|
184
|
+
##
|
|
185
|
+
## @param script_content [String] The original script content
|
|
186
|
+
##
|
|
187
|
+
## @return [Array] [modified_content, interpreter] Script content with injection added and interpreter
|
|
188
|
+
##
|
|
189
|
+
def inject_helper(script_content)
|
|
190
|
+
interpreter = detect_interpreter(script_content)
|
|
191
|
+
return [script_content, nil] unless interpreter
|
|
192
|
+
|
|
193
|
+
injection = injection_line_for(interpreter)
|
|
194
|
+
return [script_content, interpreter] unless injection
|
|
195
|
+
|
|
196
|
+
lines = script_content.lines
|
|
197
|
+
injection_lines = injection.split("\n").map { |l| "#{l}\n" }
|
|
198
|
+
# Find the hashbang line
|
|
199
|
+
if lines.first&.strip&.start_with?('#!')
|
|
200
|
+
# Insert after hashbang
|
|
201
|
+
injection_lines.each_with_index do |line, idx|
|
|
202
|
+
lines.insert(1 + idx, line)
|
|
203
|
+
end
|
|
204
|
+
else
|
|
205
|
+
# No hashbang, prepend
|
|
206
|
+
lines = injection_lines + lines
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
[lines.join, interpreter]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
##
|
|
213
|
+
## Get the command to execute a script based on interpreter
|
|
214
|
+
##
|
|
215
|
+
## @param script_path [String] Path to the script file
|
|
216
|
+
## @param interpreter [Symbol, nil] The interpreter type
|
|
217
|
+
##
|
|
218
|
+
## @return [String] Command to execute the script
|
|
219
|
+
##
|
|
220
|
+
def execution_command_for(script_path, interpreter)
|
|
221
|
+
cmd = case interpreter
|
|
222
|
+
when :bash
|
|
223
|
+
"/bin/bash #{Shellwords.escape(script_path)}"
|
|
224
|
+
when :zsh
|
|
225
|
+
"/bin/zsh #{Shellwords.escape(script_path)}"
|
|
226
|
+
when :fish
|
|
227
|
+
"/usr/bin/env fish #{Shellwords.escape(script_path)}"
|
|
228
|
+
when :ruby
|
|
229
|
+
"/usr/bin/env ruby #{Shellwords.escape(script_path)}"
|
|
230
|
+
when :python
|
|
231
|
+
"/usr/bin/env python3 #{Shellwords.escape(script_path)}"
|
|
232
|
+
when :perl
|
|
233
|
+
"/usr/bin/env perl #{Shellwords.escape(script_path)}"
|
|
234
|
+
when :node
|
|
235
|
+
"/usr/bin/env node #{Shellwords.escape(script_path)}"
|
|
236
|
+
end
|
|
237
|
+
# Fallback to direct execution if interpreter not recognized
|
|
238
|
+
cmd || script_path
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
##
|
|
242
|
+
## Install all helper scripts
|
|
243
|
+
##
|
|
244
|
+
def install_helper_scripts
|
|
245
|
+
dir = support_dir
|
|
246
|
+
FileUtils.mkdir_p(dir)
|
|
247
|
+
|
|
248
|
+
install_bash_helper(dir)
|
|
249
|
+
install_fish_helper(dir)
|
|
250
|
+
install_ruby_helper(dir)
|
|
251
|
+
install_python_helper(dir)
|
|
252
|
+
install_perl_helper(dir)
|
|
253
|
+
install_node_helper(dir)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
##
|
|
259
|
+
## Install bash/zsh helper script
|
|
260
|
+
##
|
|
261
|
+
def install_bash_helper(dir)
|
|
262
|
+
file = File.join(dir, 'howzit.sh')
|
|
263
|
+
return if File.exist?(file) && !file_stale?(file)
|
|
264
|
+
|
|
265
|
+
content = <<~BASH
|
|
266
|
+
#!/bin/bash
|
|
267
|
+
# Howzit helper functions for bash/zsh
|
|
268
|
+
|
|
269
|
+
# Log functions
|
|
270
|
+
log() {
|
|
271
|
+
local level="$1"
|
|
272
|
+
shift
|
|
273
|
+
local message="$*"
|
|
274
|
+
if [ -n "$HOWZIT_COMM_FILE" ]; then
|
|
275
|
+
echo "LOG:$level:$message" >> "$HOWZIT_COMM_FILE"
|
|
276
|
+
fi
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
log_info() { log info "$@"; }
|
|
280
|
+
log_warn() { log warn "$@"; }
|
|
281
|
+
log_error() { log error "$@"; }
|
|
282
|
+
log_debug() { log debug "$@"; }
|
|
283
|
+
|
|
284
|
+
# Set variable function
|
|
285
|
+
set_var() {
|
|
286
|
+
local var_name="$1"
|
|
287
|
+
local var_value="$2"
|
|
288
|
+
if [ -n "$HOWZIT_COMM_FILE" ]; then
|
|
289
|
+
echo "VAR:$var_name=$var_value" >> "$HOWZIT_COMM_FILE"
|
|
290
|
+
fi
|
|
291
|
+
}
|
|
292
|
+
BASH
|
|
293
|
+
|
|
294
|
+
File.write(file, content)
|
|
295
|
+
File.chmod(0o644, file)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
##
|
|
299
|
+
## Install fish helper script
|
|
300
|
+
##
|
|
301
|
+
def install_fish_helper(dir)
|
|
302
|
+
file = File.join(dir, 'howzit.fish')
|
|
303
|
+
return if File.exist?(file) && !file_stale?(file)
|
|
304
|
+
|
|
305
|
+
content = <<~FISH
|
|
306
|
+
#!/usr/bin/env fish
|
|
307
|
+
# Howzit helper functions for fish
|
|
308
|
+
|
|
309
|
+
function log -d "Log a message at the specified level"
|
|
310
|
+
set level $argv[1]
|
|
311
|
+
set -e argv[1]
|
|
312
|
+
set message (string join " " $argv)
|
|
313
|
+
if test -n "$HOWZIT_COMM_FILE"
|
|
314
|
+
echo "LOG:$level:$message" >> "$HOWZIT_COMM_FILE"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
function log_info -d "Log an info message"
|
|
319
|
+
log info $argv
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
function log_warn -d "Log a warning message"
|
|
323
|
+
log warn $argv
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
function log_error -d "Log an error message"
|
|
327
|
+
log error $argv
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
function log_debug -d "Log a debug message"
|
|
331
|
+
log debug $argv
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
function set_var -d "Set a variable for howzit"
|
|
335
|
+
set var_name $argv[1]
|
|
336
|
+
set var_value $argv[2]
|
|
337
|
+
if test -n "$HOWZIT_COMM_FILE"
|
|
338
|
+
echo "VAR:$var_name=$var_value" >> "$HOWZIT_COMM_FILE"
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
FISH
|
|
342
|
+
|
|
343
|
+
File.write(file, content)
|
|
344
|
+
File.chmod(0o644, file)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
##
|
|
348
|
+
## Install Ruby helper script
|
|
349
|
+
##
|
|
350
|
+
def install_ruby_helper(dir)
|
|
351
|
+
file = File.join(dir, 'howzit.rb')
|
|
352
|
+
return if File.exist?(file) && !file_stale?(file)
|
|
353
|
+
|
|
354
|
+
content = <<~'RUBY'
|
|
355
|
+
# frozen_string_literal: true
|
|
356
|
+
|
|
357
|
+
# Howzit helper module for Ruby
|
|
358
|
+
module Howzit
|
|
359
|
+
class << self
|
|
360
|
+
# Log methods
|
|
361
|
+
def logger
|
|
362
|
+
@logger ||= Logger.new
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
class Logger
|
|
366
|
+
def info(message)
|
|
367
|
+
log(:info, message)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def warn(message)
|
|
371
|
+
log(:warn, message)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def error(message)
|
|
375
|
+
log(:error, message)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def debug(message)
|
|
379
|
+
log(:debug, message)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
private
|
|
383
|
+
|
|
384
|
+
def log(level, message)
|
|
385
|
+
comm_file = ENV['HOWZIT_COMM_FILE']
|
|
386
|
+
return unless comm_file
|
|
387
|
+
|
|
388
|
+
File.open(comm_file, 'a') do |f|
|
|
389
|
+
f.puts "LOG:#{level}:#{message}"
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Set variable method
|
|
395
|
+
def set_var(name, value)
|
|
396
|
+
comm_file = ENV['HOWZIT_COMM_FILE']
|
|
397
|
+
return unless comm_file
|
|
398
|
+
|
|
399
|
+
File.open(comm_file, 'a') do |f|
|
|
400
|
+
f.puts "VAR:#{name}=#{value}"
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
RUBY
|
|
406
|
+
|
|
407
|
+
File.write(file, content)
|
|
408
|
+
File.chmod(0o644, file)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
##
|
|
412
|
+
## Install Python helper script
|
|
413
|
+
##
|
|
414
|
+
def install_python_helper(dir)
|
|
415
|
+
file = File.join(dir, 'howzit.py')
|
|
416
|
+
return if File.exist?(file) && !file_stale?(file)
|
|
417
|
+
|
|
418
|
+
content = <<~PYTHON
|
|
419
|
+
#!/usr/bin/env python3
|
|
420
|
+
# Howzit helper module for Python
|
|
421
|
+
|
|
422
|
+
import os
|
|
423
|
+
|
|
424
|
+
class _Logger:
|
|
425
|
+
def _log(self, level, message):
|
|
426
|
+
comm_file = os.environ.get('HOWZIT_COMM_FILE')
|
|
427
|
+
if comm_file:
|
|
428
|
+
with open(comm_file, 'a') as f:
|
|
429
|
+
f.write(f"LOG:{level}:{message}\\n")
|
|
430
|
+
|
|
431
|
+
def info(self, message):
|
|
432
|
+
self._log('info', message)
|
|
433
|
+
|
|
434
|
+
def warn(self, message):
|
|
435
|
+
self._log('warn', message)
|
|
436
|
+
|
|
437
|
+
def error(self, message):
|
|
438
|
+
self._log('error', message)
|
|
439
|
+
|
|
440
|
+
def debug(self, message):
|
|
441
|
+
self._log('debug', message)
|
|
442
|
+
|
|
443
|
+
class Howzit:
|
|
444
|
+
logger = _Logger()
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def set_var(name, value):
|
|
448
|
+
comm_file = os.environ.get('HOWZIT_COMM_FILE')
|
|
449
|
+
if comm_file:
|
|
450
|
+
with open(comm_file, 'a') as f:
|
|
451
|
+
f.write(f"VAR:{name}={value}\\n")
|
|
452
|
+
PYTHON
|
|
453
|
+
|
|
454
|
+
File.write(file, content)
|
|
455
|
+
File.chmod(0o644, file)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
##
|
|
459
|
+
## Install Perl helper script
|
|
460
|
+
##
|
|
461
|
+
def install_perl_helper(dir)
|
|
462
|
+
file = File.join(dir, 'howzit.pl')
|
|
463
|
+
return if File.exist?(file) && !file_stale?(file)
|
|
464
|
+
|
|
465
|
+
content = <<~PERL
|
|
466
|
+
#!/usr/bin/env perl
|
|
467
|
+
# Howzit helper module for Perl
|
|
468
|
+
|
|
469
|
+
package Howzit;
|
|
470
|
+
|
|
471
|
+
use strict;
|
|
472
|
+
use warnings;
|
|
473
|
+
|
|
474
|
+
sub log {
|
|
475
|
+
my ($level, $message) = @_;
|
|
476
|
+
my $comm_file = $ENV{'HOWZIT_COMM_FILE'};
|
|
477
|
+
return unless $comm_file;
|
|
478
|
+
|
|
479
|
+
open(my $fh, '>>', $comm_file) or return;
|
|
480
|
+
print $fh "LOG:$level:$message\\n";
|
|
481
|
+
close($fh);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
sub log_info { log('info', $_[0]); }
|
|
485
|
+
sub log_warn { log('warn', $_[0]); }
|
|
486
|
+
sub log_error { log('error', $_[0]); }
|
|
487
|
+
sub log_debug { log('debug', $_[0]); }
|
|
488
|
+
|
|
489
|
+
sub set_var {
|
|
490
|
+
my ($name, $value) = @_;
|
|
491
|
+
my $comm_file = $ENV{'HOWZIT_COMM_FILE'};
|
|
492
|
+
return unless $comm_file;
|
|
493
|
+
|
|
494
|
+
open(my $fh, '>>', $comm_file) or return;
|
|
495
|
+
print $fh "VAR:$name=$value\\n";
|
|
496
|
+
close($fh);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
1;
|
|
500
|
+
PERL
|
|
501
|
+
|
|
502
|
+
File.write(file, content)
|
|
503
|
+
File.chmod(0o644, file)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
##
|
|
507
|
+
## Install Node.js helper script
|
|
508
|
+
##
|
|
509
|
+
def install_node_helper(dir)
|
|
510
|
+
file = File.join(dir, 'howzit.js')
|
|
511
|
+
return if File.exist?(file) && !file_stale?(file)
|
|
512
|
+
|
|
513
|
+
content = <<~JAVASCRIPT
|
|
514
|
+
// Howzit helper module for Node.js
|
|
515
|
+
|
|
516
|
+
const fs = require('fs');
|
|
517
|
+
const path = require('path');
|
|
518
|
+
|
|
519
|
+
class Logger {
|
|
520
|
+
_log(level, message) {
|
|
521
|
+
const commFile = process.env.HOWZIT_COMM_FILE;
|
|
522
|
+
if (commFile) {
|
|
523
|
+
fs.appendFileSync(commFile, `LOG:${level}:${message}\\n`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
info(message) {
|
|
528
|
+
this._log('info', message);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
warn(message) {
|
|
532
|
+
this._log('warn', message);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
error(message) {
|
|
536
|
+
this._log('error', message);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
debug(message) {
|
|
540
|
+
this._log('debug', message);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
class Howzit {
|
|
545
|
+
static logger = new Logger();
|
|
546
|
+
|
|
547
|
+
static setVar(name, value) {
|
|
548
|
+
const commFile = process.env.HOWZIT_COMM_FILE;
|
|
549
|
+
if (commFile) {
|
|
550
|
+
fs.appendFileSync(commFile, `VAR:${name}=${value}\\n`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
module.exports = { Howzit, Logger };
|
|
556
|
+
JAVASCRIPT
|
|
557
|
+
|
|
558
|
+
File.write(file, content)
|
|
559
|
+
File.chmod(0o644, file)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
##
|
|
563
|
+
## Check if a file is stale and needs updating
|
|
564
|
+
## For now, always update to ensure latest version
|
|
565
|
+
##
|
|
566
|
+
def file_stale?(_file)
|
|
567
|
+
true
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
# rubocop:enable Metrics/ModuleLength
|
|
571
|
+
end
|
|
572
|
+
end
|
data/lib/howzit/stringutils.rb
CHANGED
|
@@ -407,15 +407,47 @@ module Howzit
|
|
|
407
407
|
end
|
|
408
408
|
|
|
409
409
|
##
|
|
410
|
-
## Examine text for
|
|
410
|
+
## Examine text for metadata and return key/value pairs
|
|
411
|
+
##
|
|
412
|
+
## Supports:
|
|
413
|
+
## - YAML front matter (starting with --- and ending with --- or ...)
|
|
414
|
+
## - MultiMarkdown-style key: value lines (up to first blank line)
|
|
411
415
|
##
|
|
412
416
|
## @return [Hash] The metadata as key/value pairs
|
|
413
417
|
##
|
|
414
418
|
def metadata
|
|
415
419
|
data = {}
|
|
416
|
-
|
|
417
|
-
|
|
420
|
+
lines = to_s.lines
|
|
421
|
+
first_idx = lines.index { |l| l !~ /^\s*$/ }
|
|
422
|
+
return {} unless first_idx
|
|
423
|
+
|
|
424
|
+
first = lines[first_idx]
|
|
425
|
+
|
|
426
|
+
if first =~ /^---\s*$/
|
|
427
|
+
# YAML front matter: between first --- and closing --- or ...
|
|
428
|
+
closing_rel = lines[(first_idx + 1)..].index { |l| l =~ /^(---|\.\.\.)\s*$/ }
|
|
429
|
+
closing_idx = closing_rel ? first_idx + 1 + closing_rel : lines.length
|
|
430
|
+
yaml_body = lines[(first_idx + 1)...closing_idx].join
|
|
431
|
+
raw = yaml_body.strip.empty? ? {} : YAML.load(yaml_body) || {}
|
|
432
|
+
if raw.is_a?(Hash)
|
|
433
|
+
raw.each do |k, v|
|
|
434
|
+
data[k.to_s.downcase] = v
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
else
|
|
438
|
+
# MultiMarkdown-style: key: value lines up to first blank line
|
|
439
|
+
header_lines = []
|
|
440
|
+
lines[first_idx..].each do |l|
|
|
441
|
+
break if l =~ /^\s*$/
|
|
442
|
+
|
|
443
|
+
header_lines << l
|
|
444
|
+
end
|
|
445
|
+
header = header_lines.join
|
|
446
|
+
header.scan(/(?mi)^(\S[\s\S]+?): ([\s\S]*?)(?=\n\S[\s\S]*?:|\Z)/).each do |m|
|
|
447
|
+
data[m[0].strip.downcase] = m[1]
|
|
448
|
+
end
|
|
418
449
|
end
|
|
450
|
+
|
|
419
451
|
out = normalize_metadata(data)
|
|
420
452
|
Howzit.named_arguments ||= {}
|
|
421
453
|
Howzit.named_arguments = out.merge(Howzit.named_arguments)
|
|
@@ -491,11 +523,21 @@ module Howzit
|
|
|
491
523
|
cols = Howzit.options[:wrap] if Howzit.options[:wrap].positive? && cols > Howzit.options[:wrap]
|
|
492
524
|
title = Color.template("#{options[:border]}#{options[:hr] * 2}( #{options[:color]}#{title}#{options[:border]} )")
|
|
493
525
|
|
|
526
|
+
# Calculate remaining width for horizontal rule, ensuring it is never negative
|
|
527
|
+
remaining = cols - title.uncolor.length
|
|
528
|
+
if should_mark_iterm?
|
|
529
|
+
# Reserve some space for the iTerm mark escape sequence in the visual layout
|
|
530
|
+
remaining -= 15
|
|
531
|
+
end
|
|
532
|
+
remaining = 0 if remaining.negative?
|
|
533
|
+
|
|
534
|
+
hr_tail = options[:hr] * remaining
|
|
494
535
|
tail = if should_mark_iterm?
|
|
495
|
-
"#{
|
|
536
|
+
"#{hr_tail}#{options[:mark] ? iterm_marker : ''}"
|
|
496
537
|
else
|
|
497
|
-
|
|
538
|
+
hr_tail
|
|
498
539
|
end
|
|
540
|
+
|
|
499
541
|
Color.template("\n\n#{title}#{tail}{x}\n\n")
|
|
500
542
|
end
|
|
501
543
|
end
|