howzit 2.1.28 → 2.1.30

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.
@@ -0,0 +1,479 @@
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
+ SUPPORT_DIR = '~/.local/share/howzit/support'
12
+
13
+ class << self
14
+ ##
15
+ ## Get the support directory path
16
+ ##
17
+ ## @return [String] expanded path to support directory
18
+ ##
19
+ def support_dir
20
+ File.expand_path(SUPPORT_DIR)
21
+ end
22
+
23
+ ##
24
+ ## Ensure support directory exists and is populated
25
+ ##
26
+ def ensure_support_dir
27
+ dir = support_dir
28
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
29
+ install_helper_scripts
30
+ dir
31
+ end
32
+
33
+ ##
34
+ ## Detect interpreter from hashbang line
35
+ ##
36
+ ## @param script_content [String] The script content
37
+ ##
38
+ ## @return [Symbol, nil] Language identifier (:bash, :zsh, :fish, :ruby, :python, etc.)
39
+ ##
40
+ def detect_interpreter(script_content)
41
+ first_line = script_content.lines.first&.strip
42
+ return nil unless first_line&.start_with?('#!')
43
+
44
+ shebang = first_line.sub(/^#!/, '').strip
45
+
46
+ case shebang
47
+ when %r{/bin/bash}, %r{/usr/bin/env bash}
48
+ :bash
49
+ when %r{/bin/zsh}, %r{/usr/bin/env zsh}
50
+ :zsh
51
+ when %r{/bin/fish}, %r{/usr/bin/env fish}
52
+ :fish
53
+ when %r{/usr/bin/env ruby}, %r{/usr/bin/ruby}, %r{/usr/local/bin/ruby}
54
+ :ruby
55
+ when %r{/usr/bin/env python3?}, %r{/usr/bin/python3?}, %r{/usr/local/bin/python3?}
56
+ :python
57
+ when %r{/usr/bin/env perl}, %r{/usr/bin/perl}
58
+ :perl
59
+ when %r{/usr/bin/env node}, %r{/usr/bin/node}
60
+ :node
61
+ end
62
+ end
63
+
64
+ ##
65
+ ## Get the injection line for a given interpreter
66
+ ##
67
+ ## @param interpreter [Symbol] The interpreter type
68
+ ##
69
+ ## @return [String, nil] The injection line to add
70
+ ##
71
+ def injection_line_for(interpreter)
72
+ support_path = support_dir
73
+ case interpreter
74
+ when :bash, :zsh
75
+ "source \"#{support_path}/howzit.sh\""
76
+ when :fish
77
+ "source \"#{support_path}/howzit.fish\""
78
+ when :ruby
79
+ "require '#{support_path}/howzit.rb'"
80
+ when :python
81
+ "import sys\nsys.path.insert(0, '#{support_path}')\nimport howzit"
82
+ when :perl
83
+ "require '#{support_path}/howzit.pl'"
84
+ when :node
85
+ "require('#{support_path}/howzit.js')"
86
+ end
87
+ end
88
+
89
+ ##
90
+ ## Inject helper script loading into script content
91
+ ##
92
+ ## @param script_content [String] The original script content
93
+ ##
94
+ ## @return [Array] [modified_content, interpreter] Script content with injection added and interpreter
95
+ ##
96
+ def inject_helper(script_content)
97
+ interpreter = detect_interpreter(script_content)
98
+ return [script_content, nil] unless interpreter
99
+
100
+ injection = injection_line_for(interpreter)
101
+ return [script_content, interpreter] unless injection
102
+
103
+ lines = script_content.lines
104
+ injection_lines = injection.split("\n").map { |l| "#{l}\n" }
105
+ # Find the hashbang line
106
+ if lines.first&.strip&.start_with?('#!')
107
+ # Insert after hashbang
108
+ injection_lines.each_with_index do |line, idx|
109
+ lines.insert(1 + idx, line)
110
+ end
111
+ else
112
+ # No hashbang, prepend
113
+ lines = injection_lines + lines
114
+ end
115
+
116
+ [lines.join, interpreter]
117
+ end
118
+
119
+ ##
120
+ ## Get the command to execute a script based on interpreter
121
+ ##
122
+ ## @param script_path [String] Path to the script file
123
+ ## @param interpreter [Symbol, nil] The interpreter type
124
+ ##
125
+ ## @return [String] Command to execute the script
126
+ ##
127
+ def execution_command_for(script_path, interpreter)
128
+ cmd = case interpreter
129
+ when :bash
130
+ "/bin/bash #{Shellwords.escape(script_path)}"
131
+ when :zsh
132
+ "/bin/zsh #{Shellwords.escape(script_path)}"
133
+ when :fish
134
+ "/usr/bin/env fish #{Shellwords.escape(script_path)}"
135
+ when :ruby
136
+ "/usr/bin/env ruby #{Shellwords.escape(script_path)}"
137
+ when :python
138
+ "/usr/bin/env python3 #{Shellwords.escape(script_path)}"
139
+ when :perl
140
+ "/usr/bin/env perl #{Shellwords.escape(script_path)}"
141
+ when :node
142
+ "/usr/bin/env node #{Shellwords.escape(script_path)}"
143
+ end
144
+ # Fallback to direct execution if interpreter not recognized
145
+ cmd || script_path
146
+ end
147
+
148
+ ##
149
+ ## Install all helper scripts
150
+ ##
151
+ def install_helper_scripts
152
+ dir = support_dir
153
+ FileUtils.mkdir_p(dir)
154
+
155
+ install_bash_helper(dir)
156
+ install_fish_helper(dir)
157
+ install_ruby_helper(dir)
158
+ install_python_helper(dir)
159
+ install_perl_helper(dir)
160
+ install_node_helper(dir)
161
+ end
162
+
163
+ private
164
+
165
+ ##
166
+ ## Install bash/zsh helper script
167
+ ##
168
+ def install_bash_helper(dir)
169
+ file = File.join(dir, 'howzit.sh')
170
+ return if File.exist?(file) && !file_stale?(file)
171
+
172
+ content = <<~BASH
173
+ #!/bin/bash
174
+ # Howzit helper functions for bash/zsh
175
+
176
+ # Log functions
177
+ log() {
178
+ local level="$1"
179
+ shift
180
+ local message="$*"
181
+ if [ -n "$HOWZIT_COMM_FILE" ]; then
182
+ echo "LOG:$level:$message" >> "$HOWZIT_COMM_FILE"
183
+ fi
184
+ }
185
+
186
+ log_info() { log info "$@"; }
187
+ log_warn() { log warn "$@"; }
188
+ log_error() { log error "$@"; }
189
+ log_debug() { log debug "$@"; }
190
+
191
+ # Set variable function
192
+ set_var() {
193
+ local var_name="$1"
194
+ local var_value="$2"
195
+ if [ -n "$HOWZIT_COMM_FILE" ]; then
196
+ echo "VAR:$var_name=$var_value" >> "$HOWZIT_COMM_FILE"
197
+ fi
198
+ }
199
+ BASH
200
+
201
+ File.write(file, content)
202
+ File.chmod(0o644, file)
203
+ end
204
+
205
+ ##
206
+ ## Install fish helper script
207
+ ##
208
+ def install_fish_helper(dir)
209
+ file = File.join(dir, 'howzit.fish')
210
+ return if File.exist?(file) && !file_stale?(file)
211
+
212
+ content = <<~FISH
213
+ #!/usr/bin/env fish
214
+ # Howzit helper functions for fish
215
+
216
+ function log -d "Log a message at the specified level"
217
+ set level $argv[1]
218
+ set -e argv[1]
219
+ set message (string join " " $argv)
220
+ if test -n "$HOWZIT_COMM_FILE"
221
+ echo "LOG:$level:$message" >> "$HOWZIT_COMM_FILE"
222
+ end
223
+ end
224
+
225
+ function log_info -d "Log an info message"
226
+ log info $argv
227
+ end
228
+
229
+ function log_warn -d "Log a warning message"
230
+ log warn $argv
231
+ end
232
+
233
+ function log_error -d "Log an error message"
234
+ log error $argv
235
+ end
236
+
237
+ function log_debug -d "Log a debug message"
238
+ log debug $argv
239
+ end
240
+
241
+ function set_var -d "Set a variable for howzit"
242
+ set var_name $argv[1]
243
+ set var_value $argv[2]
244
+ if test -n "$HOWZIT_COMM_FILE"
245
+ echo "VAR:$var_name=$var_value" >> "$HOWZIT_COMM_FILE"
246
+ end
247
+ end
248
+ FISH
249
+
250
+ File.write(file, content)
251
+ File.chmod(0o644, file)
252
+ end
253
+
254
+ ##
255
+ ## Install Ruby helper script
256
+ ##
257
+ def install_ruby_helper(dir)
258
+ file = File.join(dir, 'howzit.rb')
259
+ return if File.exist?(file) && !file_stale?(file)
260
+
261
+ content = <<~'RUBY'
262
+ # frozen_string_literal: true
263
+
264
+ # Howzit helper module for Ruby
265
+ module Howzit
266
+ class << self
267
+ # Log methods
268
+ def logger
269
+ @logger ||= Logger.new
270
+ end
271
+
272
+ class Logger
273
+ def info(message)
274
+ log(:info, message)
275
+ end
276
+
277
+ def warn(message)
278
+ log(:warn, message)
279
+ end
280
+
281
+ def error(message)
282
+ log(:error, message)
283
+ end
284
+
285
+ def debug(message)
286
+ log(:debug, message)
287
+ end
288
+
289
+ private
290
+
291
+ def log(level, message)
292
+ comm_file = ENV['HOWZIT_COMM_FILE']
293
+ return unless comm_file
294
+
295
+ File.open(comm_file, 'a') do |f|
296
+ f.puts "LOG:#{level}:#{message}"
297
+ end
298
+ end
299
+ end
300
+
301
+ # Set variable method
302
+ def set_var(name, value)
303
+ comm_file = ENV['HOWZIT_COMM_FILE']
304
+ return unless comm_file
305
+
306
+ File.open(comm_file, 'a') do |f|
307
+ f.puts "VAR:#{name}=#{value}"
308
+ end
309
+ end
310
+ end
311
+ end
312
+ RUBY
313
+
314
+ File.write(file, content)
315
+ File.chmod(0o644, file)
316
+ end
317
+
318
+ ##
319
+ ## Install Python helper script
320
+ ##
321
+ def install_python_helper(dir)
322
+ file = File.join(dir, 'howzit.py')
323
+ return if File.exist?(file) && !file_stale?(file)
324
+
325
+ content = <<~PYTHON
326
+ #!/usr/bin/env python3
327
+ # Howzit helper module for Python
328
+
329
+ import os
330
+
331
+ class _Logger:
332
+ def _log(self, level, message):
333
+ comm_file = os.environ.get('HOWZIT_COMM_FILE')
334
+ if comm_file:
335
+ with open(comm_file, 'a') as f:
336
+ f.write(f"LOG:{level}:{message}\\n")
337
+
338
+ def info(self, message):
339
+ self._log('info', message)
340
+
341
+ def warn(self, message):
342
+ self._log('warn', message)
343
+
344
+ def error(self, message):
345
+ self._log('error', message)
346
+
347
+ def debug(self, message):
348
+ self._log('debug', message)
349
+
350
+ class Howzit:
351
+ logger = _Logger()
352
+
353
+ @staticmethod
354
+ def set_var(name, value):
355
+ comm_file = os.environ.get('HOWZIT_COMM_FILE')
356
+ if comm_file:
357
+ with open(comm_file, 'a') as f:
358
+ f.write(f"VAR:{name}={value}\\n")
359
+ PYTHON
360
+
361
+ File.write(file, content)
362
+ File.chmod(0o644, file)
363
+ end
364
+
365
+ ##
366
+ ## Install Perl helper script
367
+ ##
368
+ def install_perl_helper(dir)
369
+ file = File.join(dir, 'howzit.pl')
370
+ return if File.exist?(file) && !file_stale?(file)
371
+
372
+ content = <<~PERL
373
+ #!/usr/bin/env perl
374
+ # Howzit helper module for Perl
375
+
376
+ package Howzit;
377
+
378
+ use strict;
379
+ use warnings;
380
+
381
+ sub log {
382
+ my ($level, $message) = @_;
383
+ my $comm_file = $ENV{'HOWZIT_COMM_FILE'};
384
+ return unless $comm_file;
385
+
386
+ open(my $fh, '>>', $comm_file) or return;
387
+ print $fh "LOG:$level:$message\\n";
388
+ close($fh);
389
+ }
390
+
391
+ sub log_info { log('info', $_[0]); }
392
+ sub log_warn { log('warn', $_[0]); }
393
+ sub log_error { log('error', $_[0]); }
394
+ sub log_debug { log('debug', $_[0]); }
395
+
396
+ sub set_var {
397
+ my ($name, $value) = @_;
398
+ my $comm_file = $ENV{'HOWZIT_COMM_FILE'};
399
+ return unless $comm_file;
400
+
401
+ open(my $fh, '>>', $comm_file) or return;
402
+ print $fh "VAR:$name=$value\\n";
403
+ close($fh);
404
+ }
405
+
406
+ 1;
407
+ PERL
408
+
409
+ File.write(file, content)
410
+ File.chmod(0o644, file)
411
+ end
412
+
413
+ ##
414
+ ## Install Node.js helper script
415
+ ##
416
+ def install_node_helper(dir)
417
+ file = File.join(dir, 'howzit.js')
418
+ return if File.exist?(file) && !file_stale?(file)
419
+
420
+ content = <<~JAVASCRIPT
421
+ // Howzit helper module for Node.js
422
+
423
+ const fs = require('fs');
424
+ const path = require('path');
425
+
426
+ class Logger {
427
+ _log(level, message) {
428
+ const commFile = process.env.HOWZIT_COMM_FILE;
429
+ if (commFile) {
430
+ fs.appendFileSync(commFile, `LOG:${level}:${message}\\n`);
431
+ }
432
+ }
433
+
434
+ info(message) {
435
+ this._log('info', message);
436
+ }
437
+
438
+ warn(message) {
439
+ this._log('warn', message);
440
+ }
441
+
442
+ error(message) {
443
+ this._log('error', message);
444
+ }
445
+
446
+ debug(message) {
447
+ this._log('debug', message);
448
+ }
449
+ }
450
+
451
+ class Howzit {
452
+ static logger = new Logger();
453
+
454
+ static setVar(name, value) {
455
+ const commFile = process.env.HOWZIT_COMM_FILE;
456
+ if (commFile) {
457
+ fs.appendFileSync(commFile, `VAR:${name}=${value}\\n`);
458
+ }
459
+ }
460
+ }
461
+
462
+ module.exports = { Howzit, Logger };
463
+ JAVASCRIPT
464
+
465
+ File.write(file, content)
466
+ File.chmod(0o644, file)
467
+ end
468
+
469
+ ##
470
+ ## Check if a file is stale and needs updating
471
+ ## For now, always update to ensure latest version
472
+ ##
473
+ def file_stale?(_file)
474
+ true
475
+ end
476
+ end
477
+ # rubocop:enable Metrics/ModuleLength
478
+ end
479
+ end
@@ -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..-1] =~ /#{char}/i
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 = self.length - chars.length
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, "").gsub(/\e\]1337;SetMark/, "")
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 && Howzit.named_arguments.key?(arg) && !Howzit.named_arguments[arg].nil?
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
data/lib/howzit/task.rb CHANGED
@@ -5,7 +5,7 @@ require 'English'
5
5
  module Howzit
6
6
  # Task object
7
7
  class Task
8
- attr_reader :type, :title, :action, :arguments, :parent, :optional, :default, :last_status
8
+ attr_reader :type, :title, :action, :arguments, :parent, :optional, :default, :last_status, :log_level
9
9
 
10
10
  ##
11
11
  ## Initialize a Task object
@@ -20,16 +20,18 @@ module Howzit
20
20
  ## @option attributes :title [String] task title
21
21
  ## @option attributes :action [String] task action
22
22
  ## @option attributes :parent [String] title of nested (included) topic origin
23
+ ## @option attributes :log_level [String] log level for this task (debug, info, warn, error)
23
24
  def initialize(attributes, optional: false, default: true)
24
25
  @prefix = "{bw}\u{25B7}\u{25B7} {x}"
25
26
  # arrow = "{bw}\u{279F}{x}"
26
27
  @arguments = attributes[:arguments] || []
27
28
 
28
29
  @type = attributes[:type] || :run
29
- @title = attributes[:title].nil? ? nil : attributes[:title].to_s
30
+ @title = attributes[:title]&.to_s
30
31
  @parent = attributes[:parent] || nil
31
32
 
32
33
  @action = attributes[:action].render_arguments || nil
34
+ @log_level = attributes[:log_level]
33
35
 
34
36
  @optional = optional
35
37
  @default = default
@@ -61,14 +63,34 @@ module Howzit
61
63
  Howzit.console.info "#{@prefix}{bg}Running block {bw}#{@title}{x}".c if Howzit.options[:log_level] < 2
62
64
  block = @action
63
65
  script = Tempfile.new('howzit_script')
66
+ comm_file = ScriptComm.setup
67
+ old_log_level = apply_log_level
64
68
  begin
65
- script.write(block)
69
+ # Ensure support directory exists and install helpers
70
+ ScriptSupport.ensure_support_dir
71
+ ENV['HOWZIT_SUPPORT_DIR'] = ScriptSupport.support_dir
72
+
73
+ # Inject helper script loading
74
+ modified_block, interpreter = ScriptSupport.inject_helper(block)
75
+
76
+ script.write(modified_block)
66
77
  script.close
67
- File.chmod(0o777, script.path)
68
- res = system(%(/bin/sh -c "#{script.path}"))
78
+ File.chmod(0o755, script.path)
79
+
80
+ # Use appropriate interpreter command
81
+ cmd = ScriptSupport.execution_command_for(script.path, interpreter)
82
+ # If interpreter is nil, execute directly (will respect hashbang)
83
+ res = if interpreter.nil?
84
+ system(script.path)
85
+ else
86
+ system(cmd)
87
+ end
69
88
  ensure
89
+ restore_log_level(old_log_level) if old_log_level
70
90
  script.close
71
91
  script.unlink
92
+ # Process script communication
93
+ ScriptComm.apply(comm_file) if comm_file
72
94
  end
73
95
 
74
96
  update_last_status(res ? 0 : 1)
@@ -94,6 +116,38 @@ module Howzit
94
116
  [output, matches[0].tasks.count]
95
117
  end
96
118
 
119
+ ##
120
+ ## Apply log level for this task
121
+ ##
122
+ def apply_log_level
123
+ return unless @log_level
124
+
125
+ level_map = {
126
+ 'debug' => 0,
127
+ 'info' => 1,
128
+ 'warn' => 2,
129
+ 'warning' => 2,
130
+ 'error' => 3
131
+ }
132
+ level_value = level_map[@log_level.downcase] || @log_level.to_i
133
+ old_level = Howzit.options[:log_level]
134
+ Howzit.options[:log_level] = level_value
135
+ Howzit.console.log_level = level_value
136
+ ENV['HOWZIT_LOG_LEVEL'] = @log_level.downcase
137
+ old_level
138
+ end
139
+
140
+ ##
141
+ ## Restore log level after task execution
142
+ ##
143
+ def restore_log_level(old_level)
144
+ return unless @log_level
145
+
146
+ Howzit.options[:log_level] = old_level
147
+ Howzit.console.log_level = old_level
148
+ ENV.delete('HOWZIT_LOG_LEVEL')
149
+ end
150
+
97
151
  ##
98
152
  ## Execute a run task
99
153
  ##
@@ -112,7 +166,15 @@ module Howzit
112
166
  end
113
167
  Howzit.console.info("#{@prefix}{bg}Running {bw}#{display_title}{x}".c)
114
168
  ENV['HOWZIT_SCRIPTS'] = File.expand_path('~/.config/howzit/scripts')
115
- res = system(@action)
169
+ comm_file = ScriptComm.setup
170
+ old_log_level = apply_log_level
171
+ begin
172
+ res = system(@action)
173
+ ensure
174
+ restore_log_level(old_log_level) if old_log_level
175
+ # Process script communication
176
+ ScriptComm.apply(comm_file) if comm_file
177
+ end
116
178
  update_last_status(res ? 0 : 1)
117
179
  res
118
180
  end