hiiro 0.1.377 → 0.1.378

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: 6f85065ec7cb2c8b4fe08aba7d0692ef740963b272035324d57befbf471e9e23
4
- data.tar.gz: 26745904bd4837c6b3fc2a4bf3372a2715c719af23c975b1dbcf7ad1445f430c
3
+ metadata.gz: bc8c7a68d7b0fc2d7d6182b1d307978ba4ed185d4b6162800821ef12c8fa5d4e
4
+ data.tar.gz: 3708447b7e35a09e59deb4cc570a770619054c299852e3ead032f6240180cfcf
5
5
  SHA512:
6
- metadata.gz: bcee60b2999a7acddb1918f05184ba2f84055f59440bec14b323d7a328c685cbf076a66d8c8b9753c91c92720213169c7d14b705946cf934e7382966953e9462
7
- data.tar.gz: 024bb62f15a76b36e01e102f76d2a505dd78f2db8079f19fce7d3c7e19bd3d892aac31ab45db24a5b1ecc36ef631982fa40a64b6ede114693bb7b5664f5ea8f4
6
+ metadata.gz: 0c4a5a117425f0b879607a1040d7a757b696b0b4773d8ff1cc4747a403123029217e3ec4744031ff45b14a3f5565727c8a3a2d40097a017cf8b6bcbd68b4c93f
7
+ data.tar.gz: 219ff3e0aa32313353c2aad202c745788d1a6327816a90b54e9b9adee90a975709c64a36ce40c7f5425510af1e979240eb213f496ddd81da151e39e4249ee550
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.10
data/lib/hiiro/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  class Hiiro
2
- VERSION = "0.1.377"
2
+ VERSION = "0.1.378"
3
3
  SUPPORTED_RUBY_VERSION = ">= 3.2.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.377
4
+ version: 0.1.378
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-17 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -136,6 +136,7 @@ extra_rdoc_files: []
136
136
  files:
137
137
  - ".DS_Store"
138
138
  - ".config/.keep"
139
+ - ".ruby-version"
139
140
  - AGENTS.md
140
141
  - CHANGELOG.md
141
142
  - CLAUDE.md
@@ -163,7 +164,6 @@ files:
163
164
  - bin/h-db
164
165
  - bin/h-demo
165
166
  - bin/h-docs
166
- - bin/h-docs.bkup
167
167
  - bin/h-dot
168
168
  - bin/h-dotenv
169
169
  - bin/h-dotfiles
@@ -221,7 +221,6 @@ files:
221
221
  - bin/h-zosubtask
222
222
  - bin/t
223
223
  - bin/tt
224
- - bkup..ruby-version
225
224
  - demo.sh
226
225
  - demo.typescript
227
226
  - doc/App.html
data/bin/h-docs.bkup DELETED
@@ -1,388 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
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
- require 'json'
10
- require 'time'
11
-
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')
16
-
17
- 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')
21
- option(:limit, short: :l, default: nil, desc: 'limit number of results')
22
- }
23
-
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)')
28
- }
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
85
- end
86
-
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
101
-
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
117
- end
118
-
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
129
- end
130
-
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}"
139
- end
140
-
141
- def save
142
- return true unless changed?
143
-
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')
149
- end
150
-
151
- private
152
-
153
- def load_entries
154
- @entries = JSON.parse(File.read(@index_json))
155
- rescue Errno::ENOENT, JSON::ParserError
156
- @entries = []
157
- end
158
-
159
- def key_for_file(file)
160
- File.basename(file.to_s, '.html')
161
- end
162
-
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?
166
-
167
- File.join(@docs_dir, "#{key}.html")
168
- end
169
-
170
- def html_rel_for(row)
171
- "docs/#{File.basename(row.html_path)}"
172
- end
173
-
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
188
-
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
197
-
198
- def title_for(row)
199
- return row.title if row.in_index?
200
- return markdown_title(row.md_path) if row.has_md?
201
-
202
- row.key.tr('-', ' ')
203
- end
204
-
205
- def markdown_title(path)
206
- File.foreach(path) do |line|
207
- next unless line.start_with?('# ')
208
-
209
- return line.sub(/^#\s+/, '').strip
210
- end
211
-
212
- File.basename(path, '.md').tr('-', ' ')
213
- end
214
- end
215
-
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
222
-
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
237
-
238
- def header_lines
239
- [
240
- 'h-docs tui',
241
- 'I=index entry M=markdown file H=html file',
242
- @message,
243
- ]
244
- end
245
-
246
- def footer_lines
247
- ["Showing #{top + 1}-#{top + visible_items.length} of #{items.length} docs"]
248
- end
249
-
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
268
-
269
- "#{style}#{text}\e[0m\e[K"
270
- end
271
-
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?
276
-
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
281
-
282
- [longest - min_visible_chars, 0].max
283
- end
284
-
285
- private
286
-
287
- def refresh
288
- @items = @index.rows
289
- if items.empty?
290
- @cursor = 0
291
- @top = 0
292
- return :continue
293
- end
294
-
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
- 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) }
307
- }
308
-
309
- 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) }
322
- }
323
-
324
- add_subcmd(:open) { |*args|
325
- opts = open_opts.parse(args)
326
- paths = find_docs(opts)
327
- next puts('No matching docs.') if paths.empty?
328
-
329
- targets = opts.all ? paths : [pick_one(paths)].compact
330
- targets.each { |t| system('open', t) }
331
- }
332
-
333
- 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?
337
- edit_files(*paths)
338
- }
339
-
340
- 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
350
- }
351
-
352
- 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}"
361
- }
362
-
363
- 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}"
369
- }
370
-
371
- add_subcmd(:index) { |*_args|
372
- system('open', INDEX_HTML)
373
- }
374
-
375
- 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.')
383
- }
384
-
385
- add_subcmd(:path) { |*_args|
386
- puts DOCS_DIR
387
- }
388
- }
data/bkup..ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.2.5