hiiro 0.1.379 → 0.1.381

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: 437599f5b96592172456a116919ccc4774cae2ecd248537f098f9c49f2e08446
4
- data.tar.gz: 9a892f045ab4722668b7d89fc7412ec51c3fc954ffc4dd4c1d7f60c516cdaa1e
3
+ metadata.gz: 9daba8055f86a4a6e65540732e1cc8f1eaf0a12e14bc3c8839d71ba4a72eed8c
4
+ data.tar.gz: fa3400a16b8350b4c86ae8f56111068bfbeb95fcf5f644ebf8cef7b562173377
5
5
  SHA512:
6
- metadata.gz: c55442904262705529cbc815ece19453841348d65a9c2de7601edd268b06d81cc9b8140823a4c2e55e7085169379d838543a54f11ff0f404973d4a7bf55d77c7
7
- data.tar.gz: 0c6e2780c3500669cad8748280cd3da8ecf1d19a909ef547245467ef2a9feb03441d1430696732c926b27a4c86074db4c86190961eba652387f7c2edc44009ea
6
+ metadata.gz: f928f3c7969fb291e966440aed5b37697b78214cd6d3321f8cec387f587a8436616e7cb325e19cf2cb2f770ef4c1bf1c5562ef8963d968f182cf69fac9c879ae
7
+ data.tar.gz: 387be7b9a9050dcd5b9ad76ebf1487d92d7b1af79c47d7bd282792b726f80c7f3c58d6a170d15e83b47503ae8e5336b514b183fca32c52005cad2b606055433a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.381] - 2026-09-20
4
+
5
+ ### Added
6
+ - `Hiiro::Options` now includes file/directory filtering methods: `files`, `dirs`, `file_or_dirs`, `not_files`, and `not_file_or_dirs` for convenient argument classification
7
+
3
8
  ## [0.1.377] - 2026-09-17
4
9
 
5
10
  ### Changed
data/bin/h-docs CHANGED
@@ -1,385 +1,245 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'hiiro'
4
- begin
5
- require 'hiiro/tui'
6
- rescue LoadError
7
- require File.expand_path('~/proj/hiiro/lib/hiiro/tui')
8
- end
9
4
  require 'json'
10
- require 'time'
5
+ require 'net/http'
6
+ require 'uri'
11
7
 
12
- DOCS_DIR = File.join(Dir.home, 'claude', 'docs')
13
- MDOC = File.join(Dir.home, 'bin', 'mdoc')
14
- INDEX_JSON = File.join(Dir.home, 'claude', 'index.json')
15
- INDEX_HTML = File.join(Dir.home, 'claude', 'index.html')
8
+ SERVER = ENV.fetch('HDOCS_SERVER', 'http://127.0.0.1:4387').sub(%r{/$}, '')
9
+ DOCS_DIR = File.join(Dir.home, 'claude', 'docs')
10
+ PROJECT_ROOT = ENV.fetch('PORTFOLIO_HOME', File.join(Dir.home, 'proj', 'portfolio'))
11
+ SERVICE_LABEL = "com.#{ENV.fetch('USER')}.portfolio"
12
+ SERVICE_DOMAIN = "gui/#{Process.uid}"
13
+ SERVICE_TARGET = "#{SERVICE_DOMAIN}/#{SERVICE_LABEL}"
14
+ SERVICE_PLIST = File.join(Dir.home, 'Library', 'LaunchAgents', "#{SERVICE_LABEL}.plist")
16
15
 
17
16
  ls_opts = Hiiro::Options.setup {
18
- flag(:html, short: :h, default: false, desc: 'show only .html files')
19
- flag(:md, short: :m, default: false, desc: 'show only .md files')
20
- flag(:full, short: :f, default: false, desc: 'show full absolute paths')
17
+ flag(:html, short: :h, default: false, desc: 'show HTML-backed documents')
18
+ flag(:md, short: :m, default: false, desc: 'show Markdown-backed documents')
19
+ flag(:full, short: :f, default: false, desc: 'show API URLs and source paths')
21
20
  option(:limit, short: :l, default: nil, desc: 'limit number of results')
21
+ flag(:files, default: false, desc: 'with rm, also delete the tracked source file')
22
22
  }
23
23
 
24
24
  open_opts = Hiiro::Options.setup {
25
- flag(:html, short: :h, default: false, desc: 'consider only .html files')
26
- flag(:md, short: :m, default: false, desc: 'consider only .md files')
27
- flag(:all, short: :a, default: false, desc: 'open ALL matches (skip fuzzyfind)')
25
+ flag(:html, short: :h, default: false, desc: 'consider HTML-backed documents')
26
+ flag(:md, short: :m, default: false, desc: 'consider Markdown-backed documents')
27
+ flag(:all, short: :a, default: false, desc: 'open all matches')
28
28
  }
29
29
 
30
- def file_patterns(opts)
31
- patterns = []
32
- patterns << '*.html' if opts.html
33
- patterns << '*.md' if opts.md
34
- patterns.empty? ? ['*.html', '*.md'] : patterns
35
- end
36
-
37
- def build_regex(filters)
38
- filters = ['.*'] if filters.empty?
39
- Regexp.new(filters.join(?|), Regexp::IGNORECASE)
40
- end
41
-
42
- def find_docs(opts)
43
- re = build_regex(opts.args)
44
- file_patterns(opts).flat_map { |pat| Dir[File.join(DOCS_DIR, '**', pat)] }
45
- .select { |path| File.basename(path).match?(re) }
46
- end
47
-
48
- def display(path, opts)
49
- opts.respond_to?(:full) && opts.full ? path : File.basename(path)
50
- end
51
-
52
- def pick_one(paths)
53
- return paths.first if paths.length == 1
54
- basenames = paths.map { |p| File.basename(p) }
55
- choice = fuzzyfind(basenames)
56
- return nil if choice.nil? || choice.strip.empty?
57
- paths.find { |p| File.basename(p) == choice.strip }
58
- end
59
-
60
- class DocsIndex
61
- Entry = Struct.new(:key, :entry, :md_path, :html_path, keyword_init: true) do
62
- def in_index?
63
- !entry.nil?
64
- end
65
-
66
- def has_md?
67
- md_path && File.file?(md_path)
68
- end
69
-
70
- def has_html?
71
- html_path && File.file?(html_path)
72
- end
73
-
74
- def basename
75
- "#{key}.html"
76
- end
77
-
78
- def title
79
- entry&.fetch('title', nil).to_s.empty? ? key : entry['title']
80
- end
81
-
82
- def to_s
83
- basename
84
- end
30
+ class PortfolioApi
31
+ def initialize(base_url)
32
+ @base = URI(base_url)
85
33
  end
86
34
 
87
- def initialize(docs_dir:, index_json:, mdoc:)
88
- @docs_dir = docs_dir
89
- @index_json = index_json
90
- @mdoc = mdoc
91
- @pending_delete_paths = []
92
- @changed = false
93
- load_entries
94
- end
95
-
96
- attr_reader :entries
97
-
98
- def changed?
99
- @changed
100
- end
35
+ attr_reader :base
101
36
 
102
- def rows
103
- indexed = entries.each_with_index.to_h { |entry, idx| [key_for_file(entry['file']), [entry, idx]] }
104
- keys = indexed.keys
105
- keys += Dir[File.join(@docs_dir, '*.md')].map { |path| File.basename(path, '.md') }
106
- keys += Dir[File.join(@docs_dir, '*.html')].map { |path| File.basename(path, '.html') }
107
-
108
- keys.uniq.sort_by { |key| indexed[key] ? [0, indexed[key][1]] : [1, key.downcase] }.map do |key|
109
- entry = indexed.dig(key, 0)
110
- Entry.new(
111
- key: key,
112
- entry: entry,
113
- md_path: File.join(@docs_dir, "#{key}.md"),
114
- html_path: html_path_for(key, entry)
115
- )
116
- end
37
+ def items(filters = [], limit: nil)
38
+ query = {}
39
+ query['q'] = filters.join(' ') unless filters.empty?
40
+ query['limit'] = limit.to_i if limit
41
+ get('/api/items', query).fetch('items')
117
42
  end
118
43
 
119
- def toggle(row)
120
- if row.in_index?
121
- remove(row)
122
- "Marked #{row.basename} for index removal and file deletion on save"
123
- elsif row.has_html?
124
- add(row)
125
- "Added #{row.basename} to index"
126
- else
127
- "Cannot add #{row.basename}: no html file exists"
128
- end
44
+ def health
45
+ get('/health')
129
46
  end
130
47
 
131
- def regenerate(row)
132
- return 'Regenerate requires an indexed row with markdown and missing html' unless row.in_index? && row.has_md? && !row.has_html?
133
-
134
- return "Failed to regenerate #{row.basename}" unless system(@mdoc, row.md_path)
135
-
136
- load_entries
137
- @changed = true
138
- "Regenerated #{row.basename}"
48
+ def delete(id, files: false)
49
+ target = uri("/api/items/#{id}")
50
+ target.query = URI.encode_www_form(delete_files: 1) if files
51
+ request(Net::HTTP::Delete.new(target))
52
+ true
139
53
  end
140
54
 
141
- def save
142
- return true unless changed?
55
+ def absolute_url(path)
56
+ return path if path.to_s.match?(%r{\Ahttps?://})
143
57
 
144
- @pending_delete_paths.uniq.each do |path|
145
- File.delete(path) if path && File.file?(path)
146
- end
147
- File.write(@index_json, JSON.pretty_generate(entries) + "\n")
148
- system(@mdoc, '--rebuild-index')
58
+ URI.join("#{base}/", path.to_s.sub(%r{\A/}, '')).to_s
149
59
  end
150
60
 
151
61
  private
152
62
 
153
- def load_entries
154
- @entries = JSON.parse(File.read(@index_json))
155
- rescue Errno::ENOENT, JSON::ParserError
156
- @entries = []
63
+ def get(path, query = {})
64
+ target = uri(path)
65
+ target.query = URI.encode_www_form(query) unless query.empty?
66
+ JSON.parse(request(Net::HTTP::Get.new(target)))
157
67
  end
158
68
 
159
- def key_for_file(file)
160
- File.basename(file.to_s, '.html')
69
+ def uri(path)
70
+ URI.join("#{base}/", path.sub(%r{\A/}, ''))
161
71
  end
162
72
 
163
- def html_path_for(key, entry)
164
- file = entry&.fetch('file', nil)
165
- return File.join(File.dirname(@docs_dir), file) if file && !file.empty?
73
+ def request(message)
74
+ response = Net::HTTP.start(message.uri.hostname, message.uri.port, open_timeout: 2, read_timeout: 15) { |http| http.request(message) }
75
+ raise "Portfolio API returned #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
166
76
 
167
- File.join(@docs_dir, "#{key}.html")
77
+ response.body
78
+ rescue Errno::ECONNREFUSED, SocketError => error
79
+ raise "Portfolio is unavailable at #{base}: #{error.message}"
168
80
  end
81
+ end
169
82
 
170
- def html_rel_for(row)
171
- "docs/#{File.basename(row.html_path)}"
172
- end
83
+ API = PortfolioApi.new(SERVER)
173
84
 
174
- def add(row)
175
- rel = html_rel_for(row)
176
- @pending_delete_paths.reject! { |path| path == row.md_path || path == row.html_path }
177
- entries.reject! { |entry| entry['file'] == rel }
178
- entries.unshift(
179
- 'title' => title_for(row),
180
- 'file' => rel,
181
- 'date' => File.mtime(row.html_path).strftime('%Y-%m-%d'),
182
- 'desc' => '',
183
- 'ts' => Time.now.iso8601
184
- )
185
- row.entry = entries.first
186
- @changed = true
187
- end
85
+ def source_kind?(item, opts)
86
+ return true unless opts.html || opts.md
188
87
 
189
- def remove(row)
190
- rel = html_rel_for(row)
191
- entries.reject! { |entry| entry['file'] == rel || key_for_file(entry['file']) == row.key }
192
- @pending_delete_paths << row.md_path
193
- @pending_delete_paths << row.html_path
194
- row.entry = nil
195
- @changed = true
196
- end
88
+ path = item['source_path'].to_s
89
+ (opts.html && path.end_with?('.html')) || (opts.md && path.end_with?('.md'))
90
+ end
197
91
 
198
- def title_for(row)
199
- return row.title if row.in_index?
200
- return markdown_title(row.md_path) if row.has_md?
92
+ def matching_items(opts, limit: nil)
93
+ API.items(opts.args, limit: limit).select { |item| source_kind?(item, opts) }
94
+ end
201
95
 
202
- row.key.tr('-', ' ')
203
- end
96
+ def item_label(item)
97
+ "#{item['title']} [#{item['type']} ##{item['id']}]"
98
+ end
204
99
 
205
- def markdown_title(path)
206
- File.foreach(path) do |line|
207
- next unless line.start_with?('# ')
100
+ def pick_one(items)
101
+ return items.first if items.length == 1
208
102
 
209
- return line.sub(/^#\s+/, '').strip
210
- end
103
+ labels = items.map { |item| item_label(item) }
104
+ choice = fuzzyfind(labels)
105
+ return nil if choice.nil? || choice.strip.empty?
211
106
 
212
- File.basename(path, '.md').tr('-', ' ')
213
- end
107
+ items.find { |item| item_label(item) == choice.strip }
214
108
  end
215
109
 
216
- class DocsIndexTui < Hiiro::Tui::ListScreen
217
- def initialize(index:)
218
- @index = index
219
- @message = 'space toggle index/files r regenerate missing html enter save/rebuild q cancel'
220
- super(items: index.rows, empty_message: 'No docs found.')
221
- end
110
+ def open_item(item)
111
+ system('open', API.absolute_url(item['href']))
112
+ end
222
113
 
223
- def handle_key(key)
224
- case key
225
- when ' '
226
- @message = @index.toggle(items[cursor])
227
- refresh
228
- when 'r'
229
- @message = @index.regenerate(items[cursor])
230
- refresh
231
- when :enter
232
- return @index.save
233
- else
234
- super
235
- end
236
- end
114
+ def service_loaded?
115
+ system('launchctl', 'print', SERVICE_TARGET, out: File::NULL, err: File::NULL)
116
+ end
237
117
 
238
- def header_lines
239
- [
240
- 'h-docs tui',
241
- 'I=index entry M=markdown file H=html file',
242
- @message,
243
- ]
118
+ def start_service
119
+ if service_loaded?
120
+ system('launchctl', 'kickstart', SERVICE_TARGET)
121
+ else
122
+ system('launchctl', 'bootstrap', SERVICE_DOMAIN, SERVICE_PLIST)
244
123
  end
124
+ end
245
125
 
246
- def footer_lines
247
- ["Showing #{top + 1}-#{top + visible_items.length} of #{items.length} docs"]
248
- end
126
+ def stop_service
127
+ return puts('Portfolio is not loaded.') unless service_loaded?
249
128
 
250
- def format_row(row, current, line_cols)
251
- prefix = current ? '> ' : ' '
252
- indicators = [
253
- row.in_index? ? 'I' : '-',
254
- row.has_md? ? 'M' : '-',
255
- row.has_html? ? 'H' : '-',
256
- ].join
257
- fixed = "#{prefix}#{indicators} "
258
- text_cols = [line_cols - fixed.length, 1].max
259
- text = (fixed + visible_text(row.basename, text_cols, horizontal_offset)).ljust(line_cols)
260
- style =
261
- if current
262
- "\e[7m"
263
- elsif row.in_index? && row.has_md? && !row.has_html?
264
- "\e[33m"
265
- else
266
- "\e[0m"
267
- end
129
+ system('launchctl', 'bootout', SERVICE_TARGET)
130
+ end
268
131
 
269
- "#{style}#{text}\e[0m\e[K"
132
+ def restart_service
133
+ if service_loaded?
134
+ system('launchctl', 'kickstart', '-k', SERVICE_TARGET)
135
+ else
136
+ start_service
270
137
  end
138
+ end
271
139
 
272
- def max_horizontal_offset(line_cols = nil, visible = nil)
273
- line_cols ||= [terminal_cols - 1, 1].max
274
- visible ||= visible_items
275
- return 0 if visible.empty?
140
+ Hiiro.run(*ARGV, plugins: [Pins]) {
141
+ add_subcmd(:setup, :install) { |*_args|
142
+ installer = File.join(PROJECT_ROOT, 'bin', 'install')
143
+ abort "Installer not found: #{installer}\nSet PORTFOLIO_HOME to the project directory." unless File.executable?(installer)
144
+ exit($?.exitstatus || 1) unless system(installer)
145
+ }
276
146
 
277
- fixed_width = 6
278
- remaining_cols = [line_cols - fixed_width, 1].max
279
- min_visible_chars = [5, remaining_cols].min
280
- longest = visible.map { |row| row.basename.length }.max || 0
147
+ add_subcmd(:start) { |*_args|
148
+ abort "LaunchAgent not installed. Run: h-docs setup" unless File.file?(SERVICE_PLIST)
149
+ exit($?.exitstatus || 1) unless start_service
150
+ puts 'Portfolio started.'
151
+ }
281
152
 
282
- [longest - min_visible_chars, 0].max
283
- end
153
+ add_subcmd(:stop) { |*_args|
154
+ stop_service
155
+ }
284
156
 
285
- private
157
+ add_subcmd(:restart) { |*_args|
158
+ abort "LaunchAgent not installed. Run: h-docs setup" unless File.file?(SERVICE_PLIST)
159
+ exit($?.exitstatus || 1) unless restart_service
160
+ puts 'Portfolio restarted.'
161
+ }
286
162
 
287
- def refresh
288
- @items = @index.rows
289
- if items.empty?
290
- @cursor = 0
291
- @top = 0
292
- return :continue
163
+ add_subcmd(:status) { |*_args|
164
+ if service_loaded?
165
+ system('launchctl', 'print', SERVICE_TARGET)
166
+ else
167
+ warn 'Portfolio is not loaded. Run: h-docs start'
168
+ exit 1
293
169
  end
170
+ }
294
171
 
295
- @cursor = [[cursor, 0].max, items.length - 1].min
296
- @top = [[top, 0].max, @cursor].min
297
- :continue
298
- end
299
- end
300
-
301
- Hiiro.run(*ARGV, plugins: [Pins]) {
302
172
  add_subcmd(:ls) { |*args|
303
- opts = ls_opts.parse(args)
304
- paths = find_docs(opts).sort
305
- paths = paths.last(opts.limit.to_i) if opts.limit
306
- paths.each { |p| puts display(p, opts) }
173
+ opts = ls_opts.parse(args)
174
+ matching_items(opts, limit: opts.limit).each do |item|
175
+ if opts.full
176
+ puts [API.absolute_url(item['href']), item['source_path']].compact.join("\t")
177
+ else
178
+ puts item_label(item)
179
+ end
180
+ end
307
181
  }
308
182
 
309
183
  add_subcmd(:recent) { |*args|
310
- opts = ls_opts.parse(args)
311
- numeric = opts.args.find { |a| a =~ /^\d+$/ }
312
- n = (opts.limit || numeric || 10).to_i
313
- filters = opts.args.reject { |a| a =~ /^\d+$/ }
314
- re = build_regex(filters)
315
-
316
- paths = file_patterns(opts).flat_map { |pat| Dir[File.join(DOCS_DIR, '**', pat)] }
317
- .select { |p| File.basename(p).match?(re) }
318
- .sort_by { |p| -File.mtime(p).to_i }
319
- .first(n)
320
-
321
- paths.each { |p| puts display(p, opts) }
184
+ opts = ls_opts.parse(args)
185
+ numeric = opts.args.find { |arg| arg.match?(/^\d+$/) }
186
+ count = (opts.limit || numeric || 10).to_i
187
+ opts.args.delete(numeric) if numeric
188
+ matching_items(opts, limit: count).each { |item| puts item_label(item) }
322
189
  }
323
190
 
324
191
  add_subcmd(:open) { |*args|
325
- opts = open_opts.parse(args)
326
- paths = find_docs(opts)
327
- next puts('No matching docs.') if paths.empty?
192
+ opts = open_opts.parse(args)
193
+ if opts.args.empty? && !opts.html && !opts.md && !opts.all
194
+ next system('open', SERVER)
195
+ end
196
+
197
+ items = matching_items(opts)
198
+ next puts('No matching items.') if items.empty?
328
199
 
329
- targets = opts.all ? paths : [pick_one(paths)].compact
330
- targets.each { |t| system('open', t) }
200
+ (opts.all ? items : [pick_one(items)].compact).each { |item| open_item(item) }
331
201
  }
332
202
 
333
203
  add_subcmd(:vim) { |*args|
334
- opts = ls_opts.parse(args)
335
- paths = find_docs(opts).sort
336
- next puts('No matching docs.') if paths.empty?
204
+ opts = ls_opts.parse(args)
205
+ paths = matching_items(opts).filter_map { |item| item['source_path'] if File.file?(item['source_path'].to_s) }
206
+ next puts('No matching local sources.') if paths.empty?
207
+
337
208
  edit_files(*paths)
338
209
  }
339
210
 
340
211
  add_subcmd(:cat) { |*args|
341
- opts = ls_opts.parse(args)
342
- paths = find_docs(opts).sort
343
- next puts('No matching docs.') if paths.empty?
344
-
345
- paths.each do |p|
346
- puts "===== #{File.basename(p)} ====="
347
- puts File.read(p)
348
- puts
349
- end
212
+ opts = ls_opts.parse(args)
213
+ paths = matching_items(opts).filter_map { |item| item['source_path'] if File.file?(item['source_path'].to_s) }
214
+ next puts('No matching local sources.') if paths.empty?
215
+
216
+ paths.each { |path| puts "===== #{File.basename(path)} =====", File.read(path), '' }
350
217
  }
351
218
 
352
219
  add_subcmd(:rm) { |*args|
353
- opts = ls_opts.parse(args)
354
- paths = find_docs(opts)
355
- next puts('No matching docs.') if paths.empty?
356
-
357
- target = pick_one(paths)
358
- next unless target
359
- File.delete(target)
360
- puts "Removed: #{target}"
220
+ opts = ls_opts.parse(args)
221
+ items = matching_items(opts)
222
+ next puts('No matching items.') if items.empty?
223
+
224
+ item = pick_one(items)
225
+ next unless item
226
+
227
+ API.delete(item['id'], files: opts.files)
228
+ suffix = opts.files ? ' and deleted tracked source file' : ''
229
+ puts "Removed from Portfolio#{suffix}: #{item_label(item)}"
361
230
  }
362
231
 
363
232
  add_subcmd(:count) { |*_args|
364
- html = Dir[File.join(DOCS_DIR, '**', '*.html')].count
365
- md = Dir[File.join(DOCS_DIR, '**', '*.md')].count
366
- puts "html: #{html}"
367
- puts "md: #{md}"
368
- puts "total: #{html + md}"
233
+ puts "items: #{API.health.fetch('items')}"
369
234
  }
370
235
 
371
- add_subcmd(:index) { |*_args|
372
- system('open', INDEX_HTML)
236
+ add_subcmd(:index, :tui) { |*_args|
237
+ system('open', SERVER)
373
238
  }
374
239
 
375
240
  add_subcmd(:reindex, :rebuild) { |*_args|
376
- exit($?.exitstatus || 1) unless system(MDOC, '--rebuild-index')
377
- }
378
-
379
- add_subcmd(:tui) { |*_args|
380
- index = DocsIndex.new(docs_dir: DOCS_DIR, index_json: INDEX_JSON, mdoc: MDOC)
381
- saved = DocsIndexTui.new(index: index).run
382
- puts(saved ? 'Saved docs index.' : 'Canceled docs index changes.')
241
+ status = API.health
242
+ puts "Portfolio search index is live (#{status.fetch('items')} items)."
383
243
  }
384
244
 
385
245
  add_subcmd(:path) { |*_args|
data/lib/hiiro/options.rb CHANGED
@@ -124,6 +124,12 @@ class Hiiro
124
124
  do_parse(raw_args.dup)
125
125
  end
126
126
 
127
+ def files = @files ||= args.select{|x| File.file?(x) }
128
+ def dirs = @dirs ||= args.select{|x| File.directory?(x) }
129
+ def file_or_dirs = @file_or_dirs ||= args.select{|x| File.file?(x) || File.directory?(x) }
130
+ def not_files = args - files
131
+ def not_file_or_dirs = args - file_or_dirs
132
+
127
133
  def [](name)
128
134
  @values[name.to_sym]
129
135
  end
data/lib/hiiro/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  class Hiiro
2
- VERSION = "0.1.379"
2
+ VERSION = "0.1.381"
3
3
  SUPPORTED_RUBY_VERSION = ">= 3.2.0"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.379
4
+ version: 0.1.381
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota