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 +4 -4
- data/.ruby-version +1 -0
- data/lib/hiiro/version.rb +1 -1
- metadata +3 -4
- data/bin/h-docs.bkup +0 -388
- data/bkup..ruby-version +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc8c7a68d7b0fc2d7d6182b1d307978ba4ed185d4b6162800821ef12c8fa5d4e
|
|
4
|
+
data.tar.gz: 3708447b7e35a09e59deb4cc570a770619054c299852e3ead032f6240180cfcf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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.
|
|
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-
|
|
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
|