hiiro 0.1.81 → 0.1.82

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: 3f87285ddecfdb583f432a5017f6261299b44090af65809fd4a8840f7c497a0a
4
- data.tar.gz: 41376a591fe399529e5eb4d2dd33cd01c55eb9f1a9f9f5cc1a56691608e1a631
3
+ metadata.gz: 31fd73a56f9bbcc10a8a9e62ecdd0dcbc391eb65e0bcf5886ab289986f81a198
4
+ data.tar.gz: dd392dc066b2ec7c894a2d7c2d14c8d2921f157a4b998dde9ee7ffdb83400844
5
5
  SHA512:
6
- metadata.gz: f055d30b6555898fee3011e99ed35085c0989bfb786478815c7e42ece61eba17d7e4c5462b788ca220fa1b06432ebfe17f09cf5d4a84143b8f55b0e0d31da782
7
- data.tar.gz: 40f214d34537a625a9ecea49d9ba57d9c96cf2e6a85c586b9854eabc18960d2b14009743458008c4127b822dc5c4e630db293fc55b41b0384586702ff0398029
6
+ metadata.gz: 705c6f8c3aa58a2f356272a647d67fb84a7b7c6c39b271c2372e4b3498a0d644415a75d2fad9ff16c80a73949fdc432052e6ef7d00e97149c412eb290c1fb13a
7
+ data.tar.gz: df6abb83f94c9433a992f359d66393c8c3c9db28c5d920d32529bf58a3fe536114da0396298fdf4400b062058acb054e57dc83696d5d5ada96a9319ef8ee5e86
data/bin/h-pr CHANGED
@@ -295,7 +295,7 @@ class PinnedPRManager
295
295
  end
296
296
 
297
297
  def fetch_pr_info(pr_number)
298
- fields = 'number,title,url,headRefName,state,statusCheckRollup'
298
+ fields = 'number,title,url,headRefName,state,statusCheckRollup,reviewDecision,reviews,isDraft,mergeable'
299
299
  output = `gh pr view #{pr_number} --json #{fields} 2>/dev/null`.strip
300
300
  return nil if output.empty?
301
301
  JSON.parse(output)
@@ -325,6 +325,10 @@ class PinnedPRManager
325
325
  pr['state'] = info['state']
326
326
  pr['title'] = info['title']
327
327
  pr['checks'] = summarize_checks(info['statusCheckRollup'])
328
+ pr['reviews'] = summarize_reviews(info['reviews'])
329
+ pr['review_decision'] = info['reviewDecision']
330
+ pr['is_draft'] = info['isDraft']
331
+ pr['mergeable'] = info['mergeable']
328
332
  pr['last_checked'] = Time.now.iso8601
329
333
  pr
330
334
  end
@@ -343,28 +347,124 @@ class PinnedPRManager
343
347
  { 'total' => total, 'success' => success, 'pending' => pending, 'failed' => failed }
344
348
  end
345
349
 
350
+ def summarize_reviews(reviews)
351
+ return nil unless reviews.is_a?(Array) && !reviews.empty?
352
+
353
+ # Get the latest review state per author
354
+ latest_by_author = {}
355
+ reviews.each do |review|
356
+ author = review['author']['login'] rescue nil
357
+ next unless author
358
+ # Only track if it's a meaningful state
359
+ state = review['state']
360
+ next unless %w[APPROVED CHANGES_REQUESTED COMMENTED].include?(state)
361
+ latest_by_author[author] = state
362
+ end
363
+
364
+ approved = latest_by_author.values.count { |s| s == 'APPROVED' }
365
+ changes_requested = latest_by_author.values.count { |s| s == 'CHANGES_REQUESTED' }
366
+ commented = latest_by_author.values.count { |s| s == 'COMMENTED' }
367
+
368
+ { 'approved' => approved, 'changes_requested' => changes_requested, 'commented' => commented, 'reviewers' => latest_by_author }
369
+ end
370
+
346
371
  def display_pinned(pr, idx = nil)
347
372
  num = idx ? "#{(idx + 1).to_s.rjust(3)}." : ""
348
373
  state_icon = case pr['state']
349
374
  when 'MERGED' then '[M]'
350
375
  when 'CLOSED' then '[X]'
351
- else '[O]'
376
+ else pr['is_draft'] ? '[D]' : '[O]'
352
377
  end
353
378
 
354
379
  checks_str = if pr['checks']
355
380
  c = pr['checks']
356
381
  if c['failed'] > 0
357
- " [#{c['success']}/#{c['total']} FAIL:#{c['failed']}]"
382
+ " checks:#{c['success']}/#{c['total']} FAIL:#{c['failed']}"
358
383
  elsif c['pending'] > 0
359
- " [#{c['success']}/#{c['total']} pending:#{c['pending']}]"
384
+ " checks:#{c['success']}/#{c['total']} pending:#{c['pending']}"
360
385
  else
361
- " [#{c['success']}/#{c['total']}]"
386
+ " checks:#{c['success']}/#{c['total']}"
362
387
  end
363
388
  else
364
389
  ""
365
390
  end
366
391
 
367
- "#{num} #{state_icon} ##{pr['number']} #{pr['title']}#{checks_str}".strip
392
+ reviews_str = if pr['reviews']
393
+ r = pr['reviews']
394
+ parts = []
395
+ parts << "#{r['approved']} approved" if r['approved'] > 0
396
+ parts << "#{r['changes_requested']} changes" if r['changes_requested'] > 0
397
+ parts.empty? ? "" : " | #{parts.join(', ')}"
398
+ else
399
+ ""
400
+ end
401
+
402
+ "#{num} #{state_icon} ##{pr['number']} #{pr['title']}#{checks_str}#{reviews_str}".strip
403
+ end
404
+
405
+ def display_detailed(pr, idx = nil)
406
+ lines = []
407
+ num = idx ? "#{idx + 1}." : ""
408
+
409
+ state_str = case pr['state']
410
+ when 'MERGED' then 'MERGED'
411
+ when 'CLOSED' then 'CLOSED'
412
+ else pr['is_draft'] ? 'DRAFT' : 'OPEN'
413
+ end
414
+
415
+ lines << "#{num} ##{pr['number']} - #{pr['title']}"
416
+ lines << " State: #{state_str}"
417
+ lines << " Branch: #{pr['headRefName']}" if pr['headRefName']
418
+ lines << " URL: #{pr['url']}" if pr['url']
419
+
420
+ # Checks
421
+ if pr['checks']
422
+ c = pr['checks']
423
+ check_status = if c['failed'] > 0
424
+ "FAILING (#{c['success']}/#{c['total']} passed, #{c['failed']} failed)"
425
+ elsif c['pending'] > 0
426
+ "PENDING (#{c['success']}/#{c['total']} passed, #{c['pending']} pending)"
427
+ else
428
+ "PASSING (#{c['success']}/#{c['total']})"
429
+ end
430
+ lines << " Checks: #{check_status}"
431
+ else
432
+ lines << " Checks: (none)"
433
+ end
434
+
435
+ # Reviews
436
+ if pr['reviews']
437
+ r = pr['reviews']
438
+ review_parts = []
439
+ review_parts << "#{r['approved']} approved" if r['approved'] > 0
440
+ review_parts << "#{r['changes_requested']} requesting changes" if r['changes_requested'] > 0
441
+ review_parts << "#{r['commented']} commented" if r['commented'] > 0
442
+
443
+ if review_parts.any?
444
+ lines << " Reviews: #{review_parts.join(', ')}"
445
+ if r['reviewers'] && !r['reviewers'].empty?
446
+ r['reviewers'].each do |author, state|
447
+ icon = case state
448
+ when 'APPROVED' then '+'
449
+ when 'CHANGES_REQUESTED' then '-'
450
+ else '?'
451
+ end
452
+ lines << " #{icon} #{author}: #{state.downcase.gsub('_', ' ')}"
453
+ end
454
+ end
455
+ else
456
+ lines << " Reviews: (none)"
457
+ end
458
+ else
459
+ lines << " Reviews: (not fetched)"
460
+ end
461
+
462
+ # Mergeable
463
+ if pr['mergeable']
464
+ lines << " Mergeable: #{pr['mergeable']}"
465
+ end
466
+
467
+ lines.join("\n")
368
468
  end
369
469
  end
370
470
 
@@ -586,16 +686,23 @@ hiiro.add_subcmd(:status) do |*args|
586
686
  next
587
687
  end
588
688
 
689
+ compact = args.include?('-c') || args.include?('--compact')
690
+
589
691
  puts "Refreshing status for #{pinned.length} pinned PR(s)..."
590
692
  puts
591
693
 
592
694
  pinned.each_with_index do |pr, idx|
593
695
  pinned_manager.refresh_status(pr)
594
- puts pinned_manager.display_pinned(pr, idx)
696
+ if compact
697
+ puts pinned_manager.display_pinned(pr, idx)
698
+ else
699
+ puts pinned_manager.display_detailed(pr, idx)
700
+ puts
701
+ end
595
702
  end
596
703
 
597
704
  pinned_manager.save_pinned(pinned)
598
- puts
705
+ puts "---"
599
706
  puts "Status updated at #{Time.now.strftime('%H:%M:%S')}"
600
707
  end
601
708
 
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.81"
2
+ VERSION = "0.1.82"
3
3
  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.81
4
+ version: 0.1.82
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -72,21 +72,14 @@ files:
72
72
  - bin/h-buffer
73
73
  - bin/h-commit
74
74
  - bin/h-config
75
- - bin/h-home
76
- - bin/h-html
77
75
  - bin/h-link
78
- - bin/h-mic
79
- - bin/h-note
80
76
  - bin/h-pane
81
77
  - bin/h-plugin
82
78
  - bin/h-pr
83
79
  - bin/h-project
84
- - bin/h-remind
85
- - bin/h-serve
86
80
  - bin/h-session
87
81
  - bin/h-sha
88
82
  - bin/h-todo
89
- - bin/h-video
90
83
  - bin/h-window
91
84
  - bin/h-wtree
92
85
  - docs/README.md
data/bin/h-home DELETED
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'hiiro'
4
-
5
- EDITOR = ENV.fetch('EDITOR', 'nvim')
6
- BASE_DIR = Dir.home
7
-
8
- o = Hiiro.init(*ARGV)
9
-
10
- o.add_subcmd(:edit) { |*args|
11
- system(EDITOR, __FILE__)
12
- }
13
-
14
- o.add_subcmd(:path) { |*args|
15
- print BASE_DIR
16
- }
17
-
18
- o.add_subcmd(:skf) { |*args|
19
- Dir.chdir(BASE_DIR)
20
- system('skf', *args)
21
- }
22
-
23
- o.add_subcmd(:rg) { |*args|
24
- Dir.chdir(BASE_DIR)
25
- system('rg', '-S', *args)
26
- }
27
-
28
- o.add_subcmd(:rgall) { |*args|
29
- Dir.chdir(BASE_DIR)
30
- system('rg', '-S', '--no-ignore-vcs', *args)
31
- }
32
-
33
- o.add_subcmd(:ebin) { |*args|
34
- Dir.chdir(File.join(Dir.home, 'bin'))
35
-
36
- glob_patterns = []
37
- globs = []
38
-
39
- files = args.filter_map { |arg|
40
- matches =
41
- if arg.end_with?('*')
42
- glob_patterns << arg
43
- Dir.glob(arg)
44
- elsif arg.is_a?(String)
45
- glob_patterns << '*' + arg.to_s + '*'
46
- Dir.glob('*' + arg.to_s + '*')
47
- end
48
-
49
- globs << matches
50
-
51
- matches unless matches.empty?
52
- }.flatten
53
-
54
- system(EDITOR, *files.flatten)
55
- }
56
-
57
- if o.runnable?
58
- o.run
59
- else
60
- puts :no_runnable_found
61
- end
62
-
data/bin/h-html DELETED
@@ -1,380 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require "hiiro"
3
-
4
- o = Hiiro.init(*ARGV)
5
- o.add_subcmd(:vids) { |outfile, *args|
6
- outfile = 'index.html' if outfile.nil?
7
-
8
- hash = Digest::SHA1.hexdigest(Dir.pwd)
9
- puts hash: hash
10
-
11
- videos = Dir.glob('*.mp4').map { |vid|
12
- next if File.directory?(vid)
13
-
14
- vid.inspect
15
- }.compact
16
-
17
- puts video_count: videos.count
18
-
19
- videos = videos.join(",\n")
20
-
21
- template = <<~TEMPLATE
22
- <html>
23
- <head>
24
- <style>
25
- /* a, p, video {
26
- margin-top: 20px;
27
- } */
28
- select, textarea {
29
- margin-top: 20px;
30
- width: 80%;
31
- display: block;
32
- }
33
- .flex_links {
34
- display: flex;
35
- gap: 70px;
36
- margin-top: 38px;
37
- margin-bottom: 38px;
38
- }
39
- * {
40
- font-size: 16pt;
41
- }
42
-
43
- a {
44
- font-size: 26pt;
45
- }
46
- </style>
47
- <script type="text/javascript">
48
- var favs = localStorage.getItem('favs-#{hash}') || '[]';
49
- console.log({ favs: favs });
50
-
51
- var videos = [
52
- #{videos.gsub(/\n/, "\n ")}
53
- ];
54
-
55
- function create_links() {
56
- var container = document.getElementById('links');
57
- var player = document.getElementById('player');
58
-
59
- for(var x=0; x < videos.length; x++) {
60
- var link = document.createElement('option');
61
- link.value = x;
62
- link.innerText = videos[x];
63
-
64
- container.appendChild(link);
65
- }
66
- }
67
-
68
- function load_video() {
69
- var player = document.getElementById('player');
70
- var src = document.getElementById('player_src');
71
- var links = document.getElementById('links');
72
- var idx = links.value;
73
- var id = parseInt(idx);
74
- src.src = videos[id];
75
- player.load();
76
- player.currentTime = 5;
77
- player.play();
78
- load_stats();
79
- }
80
-
81
- function save_video() {
82
- var fav_videos = localStorage.getItem('favs-#{hash}') || '[]';
83
- var json = JSON.parse(fav_videos);
84
- console.log({ favs: fav_videos, json: json });
85
-
86
- var vid = document.getElementById('links').value;
87
- vid = videos[parseInt(vid)];
88
-
89
- if(vid !== undefined) {
90
- json.push(vid);
91
-
92
- localStorage.setItem('favs-#{hash}', JSON.stringify(json));
93
- fav_videos = localStorage.getItem('favs-#{hash}') || '[]';
94
- json = JSON.parse(fav_videos);
95
- console.log({ favs: fav_videos, json: json });
96
- }
97
-
98
- display_favs();
99
- }
100
-
101
- function rm_fav() {
102
- var fav_videos = localStorage.getItem('favs-#{hash}') || '[]';
103
- var json = JSON.parse(fav_videos);
104
- console.log({ favs: fav_videos, json: json });
105
-
106
- var sel = document.getElementById('favs_select');
107
- var current_index = sel.selectedIndex;
108
- vid = json[current_index];
109
-
110
- json.splice(current_index, 1)
111
-
112
- if(vid !== undefined) {
113
- localStorage.setItem('favs-#{hash}', JSON.stringify(json));
114
- fav_videos = localStorage.getItem('favs-#{hash}') || '[]';
115
- json = JSON.parse(fav_videos);
116
- console.log({ favs: fav_videos, json: json });
117
- }
118
-
119
- display_favs();
120
-
121
- if(sel.length == 0) return;
122
-
123
- if(current_index >= sel.length) {
124
- current_index = sel.length - 1;
125
- }
126
-
127
- sel.selectedIndex = current_index;
128
- load_fav();
129
- }
130
-
131
- function load_fav() {
132
- var sel = document.getElementById('favs_select');
133
-
134
- var player = document.getElementById('player');
135
- var src = document.getElementById('player_src');
136
- src.src = sel.value;
137
- player.load();
138
- player.currentTime = 5;
139
- player.play();
140
- }
141
-
142
- function prev_fav() {
143
- var sel = document.getElementById('favs_select');
144
- if(sel.selectedIndex == 0) return;
145
- sel.selectedIndex = sel.selectedIndex - 1;
146
- load_fav();
147
- }
148
-
149
- function next_fav() {
150
- var sel = document.getElementById('favs_select');
151
- if(sel.selectedIndex >= sel.length - 1) return;
152
- sel.selectedIndex = sel.selectedIndex + 1;
153
- load_fav();
154
- }
155
-
156
- function export_favs() {
157
- var text = document.getElementById('export_favs');
158
- var data = localStorage.getItem('favs-#{hash}') || '[]';
159
- var list = JSON.parse(data);
160
- text.innerHTML = list.join("\\n");
161
- }
162
-
163
- function display_favs() {
164
- var sel = document.getElementById('favs_select');
165
- sel.innerHTML = '';
166
-
167
- var data = localStorage.getItem('favs-#{hash}') || '[]';
168
- var favs = JSON.parse(data);
169
-
170
- for(var x=0; x<favs.length; x++) {
171
- var opt = document.createElement('option');
172
- opt.value = favs[x];
173
- opt.innerText = favs[x];
174
- sel.appendChild(opt);
175
- }
176
- }
177
-
178
- function clear_last() {
179
- localStorage.removeItem('last_vid-#{hash}');
180
-
181
- show_last();
182
- }
183
-
184
- function mark_last() {
185
- var sel = document.getElementById('links');
186
- var idx = sel.value;
187
- var id = parseInt(idx);
188
-
189
- localStorage.setItem('last_vid-#{hash}', videos[id]);
190
-
191
- show_last();
192
- }
193
-
194
- function show_last() {
195
- var links = document.getElementById('links');
196
- var div = document.getElementById('last_vid');
197
- var last_vid = localStorage.getItem('last_vid-#{hash}') || 'no last vid';
198
- div.innerHTML = last_vid;
199
- var found = false;
200
- if(last_vid != 'no last vid') {
201
- for(var x=0; x<videos.length; x++) {
202
- if(videos[x] == last_vid) {
203
- found = true;
204
- links.selectedIndex = x;
205
- load_video();
206
- break;
207
- }
208
- }
209
- }
210
- if(found == false) {
211
- links.selectedIndex = 0;
212
- load_stats();
213
- }
214
- }
215
-
216
- function prev_vid() {
217
- var sel = document.getElementById('links');
218
- if(sel.selectedIndex == 0) return;
219
- sel.selectedIndex = sel.selectedIndex - 1;
220
- load_video();
221
- }
222
-
223
- function next_vid() {
224
- var sel = document.getElementById('links');
225
- if(sel.selectedIndex >= sel.length - 1) return;
226
- sel.selectedIndex = sel.selectedIndex + 1;
227
- load_video();
228
- mark_last();
229
- }
230
-
231
- function load_stats() {
232
- var sel = document.getElementById('links');
233
- var cur = document.getElementById('current_idx');
234
- var total = document.getElementById('total_links');
235
-
236
- cur.innerHTML = sel.selectedIndex + 1;
237
- total.innerHTML = sel.length;
238
- }
239
-
240
- function trash() {
241
- var sel = document.getElementById('links');
242
- var idx = sel.selectedIndex;
243
-
244
- var container = document.getElementById('trash');
245
- var link = document.createElement('option');
246
- link.value = idx;
247
- link.innerText = videos[idx];
248
-
249
- container.appendChild(link);
250
- }
251
-
252
- function clear_trash() {
253
- var container = document.getElementById('trash');
254
- container.innerHTML = '';
255
- }
256
- </script>
257
- </head>
258
- <body>
259
- :r!ls *.mp4 | sed 's@.*@&lt;video height="480" controls&gt;&lt;source src="&" type="video/mp4"&gt;&lt;/video&gt;@'
260
- <hr/>
261
-
262
- <div id="last_vid">not loaded</div>
263
- <div class="flex_links">
264
- <a href="javascript:void(0);" onclick="mark_last()" />Mark Last</a>
265
- <a href="javascript:void(0);" onclick="clear_last()" />Clear Last</a>
266
- </div>
267
-
268
- <div id="links_stats">
269
- <span id="current_idx">0</span> /
270
- <span id="total_links">99999</span>
271
- </div>
272
-
273
- <select id="links" size="5" onchange="load_stats(); load_video();">
274
- </select>
275
-
276
- <div class="flex_links">
277
- <a href="javascript:void(0);" onclick="load_video()" />Load</a>
278
- <a href="javascript:void(0);" onclick="save_video()" />Save</a>
279
- <a href="javascript:void(0);" onclick="prev_vid()">Prev Vid</a>
280
- <a href="javascript:void(0);" onclick="next_vid()">Next Vid</a>
281
- </div>
282
-
283
- <select id="favs_select" size="5" onchange="load_fav();">
284
- </select>
285
-
286
- <div class="flex_links">
287
- <a href="javascript:void(0);" onclick="rm_fav()" />Remove Fav</a>
288
- <a href="javascript:void(0);" onclick="load_fav()" />Load Fav</a>
289
- <a href="javascript:void(0);" onclick="prev_fav()" />Prev Fav</a>
290
- <a href="javascript:void(0);" onclick="next_fav()" />Next Fav</a>
291
- </div>
292
-
293
- <select id="trash" size="5">
294
- </select>
295
-
296
- <div class="flex_links">
297
- <a href="javascript:void(0);" onclick="trash()" />Trash</a>
298
- <a href="javascript:void(0);" onclick="clear_trash()" />Clear</a>
299
- </div>
300
-
301
- <textarea id="export_favs" rows="5" cols="40">
302
- </textarea>
303
-
304
- <div class="flex_links">
305
- <a href="javascript:void(0);" onclick="localStorage.removeItem('favs-#{hash}'); display_favs();" />Clear Favs</a>
306
- <a href="javascript:void(0);" onclick="export_favs()" />Export Favs</a>
307
- </div>
308
-
309
- <div id="player_wrapper">
310
- <video id="player" height="480" controls>
311
- <source id="player_src" src="">
312
- </video>
313
- </div>
314
-
315
- <script type="text/javascript">
316
- create_links();
317
- display_favs();
318
- show_last();
319
- load_stats();
320
- </script>
321
- </body>
322
- </html>
323
- TEMPLATE
324
-
325
- size = IO.write(outfile, template)
326
-
327
- puts size: size
328
- }
329
-
330
- o.add_subcmd(:videos) { |outfile, *args|
331
- puts :in_html_videos
332
-
333
- if outfile&.match?(/^-/)
334
- args = [outfile, *args]
335
- outfile = nil
336
- end
337
-
338
- outfile = 'index.html' if outfile.nil?
339
-
340
- if File.exist?(outfile) && args.none?{|a| a.match?(/-f/) }
341
- puts 'File Exists: use -f flag to force'
342
- else
343
- tags = Dir.glob('**/*').map { |vid|
344
- next if File.directory?(vid)
345
-
346
- file_cmd = format('file --mime-type "%s"', vid)
347
- result = %x[ #{file_cmd} ]
348
- next unless result.match?(/video\//)
349
-
350
- format('<p>%s</p><video height="480" controls><source src="%s"></video>', vid, vid)
351
- }.compact
352
-
353
- template = <<~TEMPLATE
354
- <html>
355
- <head>
356
- <style>
357
- a, p, video {
358
- display: block;
359
- margin-top: 20px;
360
- }
361
- </style>
362
- </head>
363
- <body>
364
- :r!ls *.mp4 | sed 's@.*@&lt;video height="480" controls&gt;&lt;source src="&" type="video/mp4"&gt;&lt;/video&gt;@'
365
- <hr/>
366
- %s
367
- </body>
368
- </html>
369
- TEMPLATE
370
-
371
- contents = format(template, tags.join("\n"))
372
- bytes = IO.write(outfile, contents)
373
-
374
- printf("video count: %d\n", tags.count)
375
- printf("bytes_written: %d\n", bytes)
376
- end
377
- }
378
-
379
- o.run
380
-