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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c70619dce51a18faa84ba9516cf1e48bb2b3028f88e22397a8690e75ae65fff
4
- data.tar.gz: 5f28a55772ca45c4f7a755b545c7fbf532460fee20533ff0714a304ea87d6799
3
+ metadata.gz: 937df7e90ef8641e22098de3ddba521506992d561bf29a225c3028fc8bbd8430
4
+ data.tar.gz: 0540d9f6a17567ca59627a64617c7f65148a0313c4d406df127fd01e50b4472d
5
5
  SHA512:
6
- metadata.gz: 73247bd6dfbc7dc0b777f88825673dc1fa36575582d29b57c8d3a2a1996d985ad01e72917b8ac3e6626119e9a1182cddad2a166850d9e1cb5bb7aa01e68ffb93
7
- data.tar.gz: 53aa254ba30a35a78880dcb7e9431d354ee4304e8244b290fb6d046dc29c60eb2becb023565ad4072e17c6af69ed5ec6faf062a15a407d87a293d7a67a14ad84
6
+ metadata.gz: bbcc5f97374ac62c822c5e0aec4d064dbe8635aabcb261cad9fe1a0ec92c1f4cf61bac0dcc2d8e330efac2dcdf141ea0e5e19ad8af307caa4782b14de1175f9a
7
+ data.tar.gz: 388f39ce1b755bf214b843ecc958bbe6058107004485be7e1aae8376177a8be720dc0c26fc863405fa0d0b6758d1bce7972a090f446b1879e1391f76cea1eed5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,98 @@
1
+ ### 2.1.31
2
+
3
+ 2026-01-06 04:57
4
+
5
+ #### CHANGED
6
+
7
+ - Updated Howzit to use XDG_CONFIG_HOME/howzit (or ~/.config/howzit if XDG_CONFIG_HOME is not set) for all configuration files, templates, themes, and script support files instead of ~/.local/share/howzit.
8
+
9
+ #### NEW
10
+
11
+ - Added automatic migration prompt that detects existing ~/.local/share/howzit directory and offers to migrate all files to the new config location, merging contents and overwriting existing files in the new location while preserving files that only exist in the new location.
12
+ - Added --migrate flag to explicitly trigger migration of legacy ~/.local/share/howzit directory to the new config location.
13
+
14
+ #### IMPROVED
15
+
16
+ - Migration prompt now appears during config initialization to catch legacy directories before creating new config files, preventing confusion about file locations.
17
+
18
+ #### FIXED
19
+
20
+ - Fixed ArgumentError when topic titles were longer than terminal width by ensuring horizontal rule width calculation never goes negative, clamping to zero when title exceeds available space.
21
+
22
+ ### 2.1.30
23
+
24
+ 2026-01-06 03:55
25
+
26
+ #### CHANGED
27
+
28
+ - Updated rubocop from version 0.93.1 to 1.82.1 for Ruby 3.4.4 compatibility
29
+ - Updated .rubocop.yml to use plugins syntax instead of require for rubocop extensions
30
+ - Updated .rubocop.yml to inherit from .rubocop_todo.yml and removed Max settings that were overriding todo file limits
31
+ - Added Security/YAMLLoad exception to .rubocop.yml to allow YAML.load usage (intentionally not using safe_load)
32
+ - Added Layout/LineLength exceptions for files with intentionally long lines (bin/howzit, task.rb, util.rb, stringutils.rb, buildnote.rb)
33
+ - Run blocks now execute scripts using appropriate interpreter commands instead of always using /bin/sh
34
+ - Moved @log_level and @set_var directive processing before task check in sequential execution to ensure they are processed correctly.
35
+
36
+ #### NEW
37
+
38
+ - Scripts can now communicate back to Howzit by writing to a communication file specified in HOWZIT_COMM_FILE environment variable, allowing scripts to send log messages (LOG:level:message) and set variables (VAR:KEY=value) that are available for subsequent tasks and conditional logic
39
+ - Added ScriptComm module to handle bidirectional communication between scripts and Howzit
40
+ - Added @if and @unless conditional blocks that allow content and tasks to be conditionally included or excluded based on evaluated conditions, with support for nested blocks
41
+ - Conditional blocks support string comparisons (==, =~ /regex/, *= contains, ^= starts with, $= ends with) and numeric comparisons (==, !=, >, >=, <, <=)
42
+ - Conditions can test against metadata keys, environment variables, positional arguments ($1, $2, etc.), named arguments, and script-set variables
43
+ - Added special condition checks: git dirty/clean, file exists <path>, dir exists <path>, topic exists <name>, and cwd/working directory
44
+ - Conditions support negation with 'not' or '!' prefix
45
+ - Added @elsif directive for alternative conditions in @if/@unless blocks, allowing multiple conditional branches
46
+ - Added @else directive for fallback branches in conditional blocks when all previous conditions are false
47
+ - Conditional blocks now support chaining multiple @elsif statements between @if/@unless and @else
48
+ - @elsif and @else work correctly with nested conditional blocks
49
+ - Added **= fuzzy match operator for string comparisons that matches if search string characters appear in order within the target string (e.g., "fluffy" **= "ffy" matches)
50
+ - Added file contents condition that reads file contents and performs string comparisons using any comparison operator (e.g., file contents VERSION.txt ^= 0.)
51
+ - File contents condition supports file paths as variables from metadata, named arguments, or environment variables
52
+ - ScriptSupport module provides helper functions (log_info, log_warn, log_error, log_debug, set_var) for bash, zsh, fish, ruby, python, perl, and node scripts in run blocks
53
+ - Automatic interpreter detection from hashbang lines in scripts
54
+ - Helper script injection into run blocks based on detected interpreter
55
+ - Support directory installation at ~/.local/share/howzit/support with language-specific helper scripts
56
+ - Add sequential conditional evaluation: @if/@unless blocks are now evaluated after each task runs, allowing variables set by scripts to affect subsequent conditional blocks in the same topic
57
+ - Add @log_level(LEVEL) directive to set log level for subsequent tasks in a topic
58
+ - Add log_level parameter to @run directives (e.g., @run(script.sh, log_level=debug))
59
+ - Add HOWZIT_LOG_LEVEL environment variable support for global log level configuration
60
+ - Add emoji and color indicators for log messages (debug, info, warn, error)
61
+ - Add comprehensive test coverage for sequential conditional evaluation including @if/@unless/@elsif/@else blocks with variables from run blocks
62
+ - Add comprehensive test coverage for log level configuration including @log_level directive, log_level parameter in @run directives, and HOWZIT_LOG_LEVEL environment variable
63
+ - Added @set_var directive to set variables in build notes. Takes two comma-separated arguments: variable name (alphanumeric, dashes, underscores only) and value. Variables are available as ${VAR} in subsequent @run directives, run blocks, and @if/@else conditional blocks.
64
+ - Added command substitution support to @set_var directive. Values can use backticks (`command`) or $() syntax ($(command)) to execute commands and use their output as the variable value. Commands can reference other variables using ${VAR} syntax.
65
+ - Added @set_var directive to set variables directly in build notes, making them available as ${VAR} in subsequent @run directives, run blocks, and @if/@else conditional blocks.
66
+ - Added command substitution support to @set_var so values can come from backtick commands (`command`) or $() syntax ($(command)), with command output (whitespace stripped) used as the variable value and ${VAR} substitutions applied inside the command.
67
+
68
+ #### IMPROVED
69
+
70
+ - Auto-corrected rubocop style offenses including string literals, redundant self, parentheses, and other correctable issues
71
+ - Fixed Lint/Void issue in buildnote.rb by simplifying conditional logic
72
+ - Cwd and working directory can now be used with string comparison operators (==, =~, *=, ^=, $=) to check the current directory path
73
+ - Conditions now support ${var} syntax in addition to var for consistency with variable substitution syntax
74
+ - String comparison operators (*=, ^=, $=) now treat unquoted strings that aren't found as variables as literal strings, allowing simpler syntax like template *= gem instead of template *= "gem"
75
+ - Log messages from scripts now display with visual indicators: debug messages show with magnifying glass emoji and dark color, info with info emoji and cyan, warnings with warning emoji and yellow, errors with X emoji and red
76
+ - Log level filtering now properly applies to script-to-howzit communication messages, showing only messages at or above the configured level
77
+ - Conditional blocks (@if/@unless/@elsif/@else) now re-evaluate after each task execution, enabling dynamic conditional flow based on variables set by preceding tasks
78
+ - Improve task directive parsing by refactoring to use unless/next pattern for better code organization and fixing @log_level directive handling
79
+ - Improve Directive#to_task to properly handle title rendering with variable substitution, argument parsing for include tasks, and action escaping for copy tasks
80
+ - Processed @set_var directives before task creation in topics without conditionals so variable substitution in @run actions works as expected even in the non-sequential execution path.
81
+
82
+ #### FIXED
83
+
84
+ - Resolved NameError for 'white' color method by generating escape codes directly from configured_colors hash instead of calling dynamically generated methods
85
+ - Fixed infinite recursion in ConsoleLogger by using $stderr.puts directly instead of calling warn method recursively
86
+ - Color template method now properly respects coloring? setting and returns empty strings when coloring is disabled
87
+ - Resolved test failures caused by Howzit.buildnote caching stale instances by resetting @buildnote in spec_helper before each test
88
+ - Fixed bug where @end statements failed to close conditional blocks when conditions evaluated to false, preventing subsequent conditional blocks from working correctly
89
+ - Fixed issue where named arguments from topic titles were not available when evaluating conditions in conditional blocks
90
+ - Suppressed EPIPE errors that occur when writing to stdout/stderr after pipes are closed, preventing error messages from appearing in terminal output
91
+ - Fix @elsif and @else conditional blocks not executing tasks when parent @if condition is false by correctly tracking branch indices and skipping parent @if index in conditional path evaluation
92
+ - Fix clipboard copy test failing due to cached console logger instance not updating when log_level option changes
93
+ - Fixed variable persistence issue in sequential execution where Howzit.named_arguments was being reset on each iteration, causing @set_var variables to be lost.
94
+ - Ensured variables set by @set_var and helper scripts persist correctly across sequential conditional evaluation by merging topic named arguments into Howzit.named_arguments instead of overwriting them.
95
+
1
96
  ### 2.1.29
2
97
 
3
98
  2026-01-01 06:55
data/README.md CHANGED
@@ -4,11 +4,11 @@
4
4
  [![Gem](https://img.shields.io/gem/v/howzit.svg)](https://rubygems.org/gems/howzit)
5
5
  [![GitHub license](https://img.shields.io/github/license/ttscoff/howzit.svg)](./LICENSE.txt)
6
6
 
7
+
7
8
  A command-line reference tool for tracking project build systems
8
9
 
9
10
  Howzit is a tool that allows you to keep Markdown-formatted notes about a project's tools and procedures. It functions as an easy lookup for notes about a particular task, as well as a task runner to automatically execute appropriate commands.
10
11
 
11
-
12
12
  ## Features
13
13
 
14
14
  - Match topic titles with any portion of title
@@ -16,6 +16,10 @@ Howzit is a tool that allows you to keep Markdown-formatted notes about a projec
16
16
  - Use `@run()`, `@copy()`, and `@open()` to perform actions within a build notes file
17
17
  - Use `@include()` to import another topic's tasks
18
18
  - Use fenced code blocks to include/run embedded scripts
19
+ - Scripts can communicate back to Howzit, sending log messages and setting variables
20
+ - Conditional blocks (`@if`/`@unless`/`@elsif`/`@else`) for conditionally including content and tasks
21
+ - String comparison operators including fuzzy match (`**=`) for flexible pattern matching
22
+ - File contents conditions to check file contents in conditional blocks
19
23
  - Sets iTerm 2 marks on topic titles for navigation when paging is disabled
20
24
  - Inside of git repositories, howzit will work from subdirectories, assuming build notes are in top level of repo
21
25
  - Templates for easily including repeat tasks
data/Rakefile CHANGED
@@ -46,9 +46,15 @@ task :ver do
46
46
  puts "changelog: #{cver}"
47
47
  end
48
48
 
49
+ desc 'Version.rb check'
50
+ task :vver do
51
+ res = `grep VERSION lib/howzit/version.rb`
52
+ print res.match(/VERSION *= *['"](\d+\.\d+\.\d+(\w+)?)/)[1]
53
+ end
54
+
49
55
  desc 'Changelog version check'
50
56
  task :cver do
51
- puts IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
57
+ print IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
52
58
  end
53
59
 
54
60
  desc 'Run tests in Docker'
data/bin/howzit CHANGED
@@ -248,6 +248,11 @@ OptionParser.new do |opts|
248
248
  Process.exit 0
249
249
  end
250
250
 
251
+ opts.on('--migrate', 'Migrate legacy files from ~/.local/share/howzit to the new config location') do
252
+ Howzit::ScriptSupport.migrate_legacy_support(early_init: false)
253
+ Process.exit 0
254
+ end
255
+
251
256
  opts.on('-v', '--version', 'Display version number') do
252
257
  puts "#{File.basename(__FILE__)} v#{Howzit::VERSION}"
253
258
  Process.exit 0
@@ -21,9 +21,31 @@ module Howzit
21
21
  content = Util.read_file(file)
22
22
  raise "{br}No content found in build note (#{file}){x}".c if content.nil? || content.empty?
23
23
 
24
+ # Global metadata from config (e.g. ~/.config/howzit/howzit.yml)
25
+ # Expecting something like:
26
+ # metadata:
27
+ # author: Brett Terpstra
28
+ # license: MIT
29
+ global_meta = {}
30
+ raw_global = Howzit.options[:metadata] || Howzit.options['metadata']
31
+ if raw_global.is_a?(Hash)
32
+ raw_global.each do |k, v|
33
+ global_meta[k.to_s.downcase] = v
34
+ end
35
+ end
36
+
37
+ # Metadata defined in the build note itself (before the first heading)
24
38
  this_meta = content.split(/^#/)[0].strip.metadata
25
39
 
26
- @metadata = meta.nil? ? this_meta : meta.merge(this_meta)
40
+ # Merge order:
41
+ # 1. Global config metadata (lowest precedence)
42
+ # 2. Metadata passed in via meta argument (e.g. templates)
43
+ # 3. Build note metadata (highest precedence)
44
+ combined_meta = global_meta.dup
45
+ combined_meta.merge!(meta) if meta
46
+ combined_meta.merge!(this_meta) if this_meta
47
+
48
+ @metadata = combined_meta
27
49
 
28
50
  read_help(file)
29
51
  end
data/lib/howzit/config.rb CHANGED
@@ -194,6 +194,19 @@ module Howzit
194
194
  config = load_config
195
195
  load_theme
196
196
  @options = flags.merge(config)
197
+
198
+ # Check for HOWZIT_LOG_LEVEL environment variable
199
+ return unless ENV['HOWZIT_LOG_LEVEL']
200
+
201
+ level_str = ENV['HOWZIT_LOG_LEVEL'].downcase
202
+ level_map = {
203
+ 'debug' => 0,
204
+ 'info' => 1,
205
+ 'warn' => 2,
206
+ 'warning' => 2,
207
+ 'error' => 3
208
+ }
209
+ @options[:log_level] = level_map[level_str] || level_str.to_i
197
210
  end
198
211
 
199
212
  ##
@@ -238,6 +251,14 @@ module Howzit
238
251
  ## @param default [Hash] default configuration to write
239
252
  ##
240
253
  def create_config(default)
254
+ # If a legacy ~/.local/share/howzit directory exists, offer to migrate it
255
+ # into the new config root before creating any new files to avoid confusion
256
+ # about where Howzit stores its configuration.
257
+ # Use early_init=true since we're called during config initialization and can't access Howzit.options yet
258
+ if defined?(Howzit::ScriptSupport) && File.directory?(File.expand_path(Howzit::ScriptSupport::LEGACY_SUPPORT_DIR))
259
+ Howzit::ScriptSupport.migrate_legacy_support(early_init: true)
260
+ end
261
+
241
262
  unless File.directory?(config_dir)
242
263
  Howzit::ConsoleLogger.new(1).info "Creating config directory at #{config_dir}"
243
264
  FileUtils.mkdir_p(config_dir)
@@ -31,6 +31,50 @@ module Howzit
31
31
  @log_level = Howzit.options[:log_level]
32
32
  end
33
33
 
34
+ ##
35
+ ## Get emoji for log level
36
+ ##
37
+ ## @param level [Symbol] The level
38
+ ##
39
+ ## @return [String] Emoji for the level
40
+ ##
41
+ def emoji_for_level(level)
42
+ case level
43
+ when :debug
44
+ '🔍'
45
+ when :info
46
+ 'ℹ️'
47
+ when :warn
48
+ '⚠️'
49
+ when :error
50
+ '❌'
51
+ else
52
+ ''
53
+ end
54
+ end
55
+
56
+ ##
57
+ ## Get color prefix for log level
58
+ ##
59
+ ## @param level [Symbol] The level
60
+ ##
61
+ ## @return [String] Color template string
62
+ ##
63
+ def color_for_level(level)
64
+ case level
65
+ when :debug
66
+ '{d}'
67
+ when :info
68
+ '{c}'
69
+ when :warn
70
+ '{y}'
71
+ when :error
72
+ '{r}'
73
+ else
74
+ ''
75
+ end
76
+ end
77
+
34
78
  ##
35
79
  ## Write a message to the console based on the urgency
36
80
  ## level and user's log level setting
@@ -41,8 +85,16 @@ module Howzit
41
85
  def write(msg, level = :info)
42
86
  return unless LOG_LEVELS[level] >= @log_level
43
87
 
88
+ emoji = emoji_for_level(level)
89
+ color = color_for_level(level)
90
+ formatted_msg = if emoji && color
91
+ "#{emoji} #{color}#{msg}{x}".c
92
+ else
93
+ msg
94
+ end
95
+
44
96
  begin
45
- $stderr.puts msg
97
+ $stderr.puts formatted_msg
46
98
  rescue Errno::EPIPE
47
99
  # Pipe closed, ignore
48
100
  end
@@ -74,8 +126,16 @@ module Howzit
74
126
  def warn(msg)
75
127
  return unless LOG_LEVELS[:warn] >= @log_level
76
128
 
129
+ emoji = emoji_for_level(:warn)
130
+ color = color_for_level(:warn)
131
+ formatted_msg = if emoji && color
132
+ "#{emoji} #{color}#{msg}{x}".c
133
+ else
134
+ msg
135
+ end
136
+
77
137
  begin
78
- $stderr.puts msg
138
+ $stderr.puts formatted_msg
79
139
  rescue Errno::EPIPE
80
140
  # Pipe closed, ignore
81
141
  end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ module Howzit
6
+ # Directive class
7
+ # Represents a parsed directive from topic content (tasks, conditionals, etc.)
8
+ class Directive
9
+ attr_reader :type, :content, :condition, :directive_type, :optional, :default, :line_number, :conditional_path,
10
+ :log_level_value, :var_name, :var_value
11
+
12
+ ##
13
+ ## Initialize a Directive
14
+ ##
15
+ ## @param type [Symbol] :task, :if, :unless, :elsif, :else, :end, :log_level
16
+ ## @param content [String] The directive content (action, block, etc.)
17
+ ## @param condition [String, nil] Condition string for conditionals
18
+ ## @param directive_type [String, nil] 'if', 'unless', 'elsif', 'else' for conditionals
19
+ ## @param optional [Boolean] Whether task requires confirmation
20
+ ## @param default [Boolean] Default response for confirmation
21
+ ## @param line_number [Integer] Line number in original content
22
+ ## @param conditional_path [Array] Array of conditional indices this directive is nested in
23
+ ## @param log_level_value [String, nil] Log level value for @log_level directives
24
+ ## @param var_name [String, nil] Variable name for @set_var directives
25
+ ## @param var_value [String, nil] Variable value for @set_var directives
26
+ ##
27
+ def initialize(type:, content: nil, condition: nil, directive_type: nil, optional: false, default: true,
28
+ line_number: nil, conditional_path: [], log_level_value: nil, var_name: nil, var_value: nil)
29
+ @type = type
30
+ @content = content
31
+ @condition = condition
32
+ @directive_type = directive_type
33
+ @optional = optional
34
+ @default = default
35
+ @line_number = line_number
36
+ @conditional_path = conditional_path || []
37
+ @log_level_value = log_level_value
38
+ @var_name = var_name
39
+ @var_value = var_value
40
+ end
41
+
42
+ ##
43
+ ## Is this a conditional directive?
44
+ ##
45
+ def conditional?
46
+ %i[if unless elsif else end].include?(@type)
47
+ end
48
+
49
+ ##
50
+ ## Is this a task directive?
51
+ ##
52
+ def task?
53
+ @type == :task
54
+ end
55
+
56
+ ##
57
+ ## Is this a log_level directive?
58
+ ##
59
+ def log_level?
60
+ @type == :log_level
61
+ end
62
+
63
+ ##
64
+ ## Is this a set_var directive?
65
+ ##
66
+ def set_var?
67
+ @type == :set_var
68
+ end
69
+
70
+ ##
71
+ ## Convert directive to a Task object (only works for task directives)
72
+ ##
73
+ ## @param parent [Topic] The parent topic
74
+ ## @param current_log_level [String, nil] Current log level to apply to task
75
+ ##
76
+ ## @return [Task] Task object
77
+ ##
78
+ def to_task(parent, current_log_level: nil)
79
+ return nil unless task?
80
+
81
+ task_data = @content.dup
82
+ task_type = task_data[:type]
83
+
84
+ # Apply current log level if set and task doesn't have its own
85
+ task_data[:log_level] = current_log_level if current_log_level && !task_data[:log_level]
86
+
87
+ # Set named_arguments before processing titles for variable substitution
88
+ Howzit.named_arguments = parent.named_args
89
+
90
+ case task_type
91
+ when :block
92
+ # Block tasks are already properly formatted
93
+ task_data[:parent] = parent
94
+ Howzit::Task.new(task_data, optional: @optional, default: @default)
95
+ when :run
96
+ # Run tasks need title rendering (similar to define_task_args)
97
+ title = task_data[:title]
98
+ title = title.render_arguments if title && !title.empty?
99
+ task_data[:title] = title
100
+ task_data[:parent] = parent
101
+ Howzit::Task.new(task_data, optional: @optional, default: @default)
102
+ when :copy
103
+ # Copy tasks need title rendering and action escaping
104
+ title = task_data[:title]
105
+ title = title.render_arguments if title && !title.empty?
106
+ task_data[:title] = title
107
+ task_data[:action] = Shellwords.escape(task_data[:action])
108
+ task_data[:parent] = parent
109
+ Howzit::Task.new(task_data, optional: @optional, default: @default)
110
+ when :open
111
+ # Open tasks need title rendering
112
+ title = task_data[:title]
113
+ title = title.render_arguments if title && !title.empty?
114
+ task_data[:title] = title
115
+ task_data[:parent] = parent
116
+ Howzit::Task.new(task_data, optional: @optional, default: @default)
117
+ when :include
118
+ # Include tasks need special handling (title processing, arguments, etc.)
119
+ title = task_data[:title]
120
+ if title =~ /\[(.*?)\] *$/
121
+ args = Regexp.last_match(1).split(/ *, */).map(&:render_arguments)
122
+ Howzit.arguments = args
123
+ parent.arguments
124
+ title.sub!(/ *\[.*?\] *$/, '')
125
+ end
126
+ title = title.render_arguments if title && !title.empty?
127
+ task_data[:title] = title
128
+ task_data[:parent] = parent
129
+ task_data[:arguments] = Howzit.named_arguments
130
+ Howzit::Task.new(task_data, optional: @optional, default: @default)
131
+ else
132
+ task_data[:parent] = parent
133
+ Howzit::Task.new(task_data, optional: @optional, default: @default)
134
+ end
135
+ end
136
+ end
137
+ end