rufio 0.91.0 → 1.0.0

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: 0c50fc72fd43046f6fb861b05ff378c5122ebfa1bba180463340a5f1e906ad3f
4
- data.tar.gz: 7e5fcc725fafc5ceeeb90433a5cb767e3d0c98431a0b5bdb554f727a99fb50f5
3
+ metadata.gz: 78138156eb9ae86f5a57696e3b0170417da3c7b2c8a2d0dba4af3460efe69ddd
4
+ data.tar.gz: b8c184800c8e26955663e0404654e50973407441ac61658511b15fc2bbf6c96f
5
5
  SHA512:
6
- metadata.gz: 5eb8c2b7955f69c822d58aaf976be1669aa446d12eacbac4fe24c0e152ffd6d79631488e6c0debdb7e232b477321ee4d5617873e06be5da1999704b28fe4554f
7
- data.tar.gz: fd71f1119f3ae724ecd8c220c6c9401a7dfd4d4fa40108aa2d5320afd23a2c181589224cbd157a28f8cbbcad847f48285bc4108cd583cec93230e74466cdafd0
6
+ metadata.gz: 572569248e94a4456f20bcbf7a8235b26bbae55aadc24c911ae528ebcb23b08f2cd0ec1cb9f97c6781308b6856e676b4983e42728bd21ae4a21e0820b0eb0e28
7
+ data.tar.gz: 65d4deb78ca48dc9e892b63e42bd98827a50d1b0168f7df198957c476696d67999268f4e2021bbc4beb6edad5be96e8acbac6a087bfe5855e25ac63bb23be102
data/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ All notable changes to rufio will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.0] - 2026-04-28
9
+
10
+ ### Added
11
+ - **Job mode: View log with Space key**: Press Space on a selected job to display its full execution log. Footer switches to `[ESC] Close Log` while in log view; press ESC to return to the job list
12
+ - **Job mode: Disable Tab key**: Tab (bookmark cycling) is now suppressed in job mode to prevent accidental navigation. Removed `[Tab] Switch Mode` from the job mode footer
13
+
14
+ ### Fixed
15
+ - **Windows: ESC key not working**: `STDIN.raw` + `getbyte` silently drops the ESC byte (0x1B) under Windows ConPTY. The blocking input thread now uses `getch` instead, pushing each received byte onto the queue
16
+ - **Windows: Preview flickering**: Redundant `print "\e[2J\e[H"` calls before `renderer.clear` caused the screen to blank momentarily on every redraw. Removed the duplicate clears in `refresh_display`, `set_job_mode`, and `exit_job_mode`. Also unified the async highlight-updated flag into `UIRenderer` (it was maintained separately in both `TerminalUI` and `UIRenderer`, causing missed redraws)
17
+ - **Windows: Cursor visible as blinking vertical bar in bottom-right corner**: `tput civis` is a no-op on Windows, leaving the cursor visible. Replaced `tput civis/cnorm/smcup/rmcup` with ANSI sequences `\e[?25l`/`\e[?25h`/`\e[?1049h`/`\e[?1049l` which work on Windows Terminal. Cursor is shown only during command mode input and hidden otherwise
18
+ - **Windows: `bundle install` fails with "cannot load git ls-files"**: `rufio.gemspec` now falls back to `Dir.glob` when `git` is not on PATH
19
+
8
20
  ## [0.91.0] - 2026-04-12
9
21
 
10
22
  ### Changed
@@ -669,7 +669,6 @@ module Rufio
669
669
  exit_job_mode
670
670
  true
671
671
  when :show_log
672
- # ログ表示は将来実装
673
672
  @terminal_ui&.trigger_job_mode_redraw if @terminal_ui
674
673
  true
675
674
  when true, false
@@ -118,7 +118,8 @@ module Rufio
118
118
 
119
119
  begin
120
120
  Timeout.timeout(timeout_sec) do
121
- stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, *command, **options)
121
+ args = env.empty? ? [*command] : [env, *command]
122
+ stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(*args, **options)
122
123
  pid = wait_thread.pid
123
124
  stdin.close
124
125
  stdout = stdout_io.read
@@ -162,7 +163,9 @@ module Rufio
162
163
 
163
164
  # タイムアウトなしで実行
164
165
  def execute_without_timeout(command, env, options)
165
- stdout, stderr, status = Open3.capture3(env, *command, **options)
166
+ # 空の env ハッシュを渡すと Windows SystemRoot 等が引き継がれないため省略する
167
+ args = env.empty? ? [*command] : [env, *command]
168
+ stdout, stderr, status = Open3.capture3(*args, **options)
166
169
 
167
170
  {
168
171
  success: status.success?,
@@ -183,7 +186,8 @@ module Rufio
183
186
 
184
187
  begin
185
188
  Timeout.timeout(timeout_sec) do
186
- stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(env, shell_command, **options)
189
+ args = env.empty? ? [shell_command] : [env, shell_command]
190
+ stdin, stdout_io, stderr_io, wait_thread = Open3.popen3(*args, **options)
187
191
  pid = wait_thread.pid
188
192
  stdin.close
189
193
  stdout = stdout_io.read
@@ -226,7 +230,8 @@ module Rufio
226
230
 
227
231
  # シェルコマンドをタイムアウトなしで実行
228
232
  def execute_shell_without_timeout(shell_command, env, options)
229
- stdout, stderr, status = Open3.capture3(env, shell_command, **options)
233
+ args = env.empty? ? [shell_command] : [env, shell_command]
234
+ stdout, stderr, status = Open3.capture3(*args, **options)
230
235
 
231
236
  {
232
237
  success: status.success?,
@@ -48,6 +48,8 @@ module Rufio
48
48
  @running = false
49
49
  @test_mode = test_mode
50
50
  @multibyte_reader = MultibyteInputReader.new(STDIN)
51
+ @input_queue = nil
52
+ @input_thread = nil
51
53
  @command_mode_active = false
52
54
  @command_input = ""
53
55
  @command_mode = CommandMode.new
@@ -73,8 +75,6 @@ module Rufio
73
75
 
74
76
  # シンタックスハイライター(bat が利用可能な場合のみ動作)
75
77
  @syntax_highlighter = SyntaxHighlighter.new
76
- # 非同期ハイライト完了フラグ(Thread → メインループへの通知)
77
- @highlight_updated = false
78
78
 
79
79
 
80
80
  # Tab mode manager
@@ -132,7 +132,6 @@ module Rufio
132
132
  def refresh_display
133
133
  # ウィンドウサイズを更新してから画面をクリアして再描画
134
134
  update_screen_size
135
- print "\e[2J\e[H" # clear screen, cursor to home
136
135
 
137
136
  # プレビューキャッシュをクリア(ディレクトリ変更やリフレッシュ時)
138
137
  @preview_cache.clear
@@ -176,16 +175,34 @@ module Rufio
176
175
  # ブックマークハイライトが期限切れかどうか
177
176
  # @return [Boolean] true=期限切れ or ハイライト中でない, false=ハイライト中
178
177
  def setup_terminal
179
- # terminal setup
180
- system('tput smcup') # alternate screen
181
- system('tput civis') # cursor invisible
182
- print "\e[2J\e[H" # clear screen, cursor to home (first time only)
178
+ # terminal setup(ANSI エスケープで統一 — tput は Windows で動作しない)
179
+ print "\e[?1049h" # alternate screen buffer
180
+ print "\e[?25l" # cursor invisible
181
+ print "\e[2J\e[H" # clear screen, cursor to home (first time only)
183
182
 
184
183
  # rawモードに設定(ゲームループのノンブロッキング入力用)
185
184
  if STDIN.tty?
186
185
  STDIN.raw!
187
186
  end
188
187
 
188
+ # Windows: ブロッキング読み取りスレッドを起動。
189
+ # ConPTY パイプでは IO.select のシグナル遅延で ESC を取りこぼすため、
190
+ # 常にブロッキングで読み取りキューに積む。
191
+ # getbyte は raw モード下で ESC (0x1B) を取りこぼす ConPTY バグがあるため
192
+ # getch を使用し、受信した各バイトをキューに積む。
193
+ if windows?
194
+ @input_queue = Queue.new
195
+ @input_thread = Thread.new do
196
+ loop do
197
+ ch = STDIN.getch(min: 1)
198
+ break if ch.nil?
199
+ ch.bytes.each { |b| @input_queue << b }
200
+ end
201
+ rescue
202
+ # スレッド終了
203
+ end
204
+ end
205
+
189
206
  # SGR拡張マウスレポートを有効化
190
207
  # Unix: \e[?1003h (any-event) + \e[?1006h (SGR座標) → クリック・スクロール・移動
191
208
  # Windows: \e[?1003h は Windows Terminal / conhost 非対応。
@@ -215,29 +232,72 @@ module Rufio
215
232
  end
216
233
 
217
234
  # エスケープシーケンスの後続バイトを読み取る(矢印キー等の短いシーケンス用)。
218
- # Windows: IO.select(timeout=0) がESCを取りこぼすため 5ms タイムアウトを使用。
235
+ # Windows: IO.select を使わずキューから取得(5ms タイムアウト)。
219
236
  def read_next_input_byte
220
237
  if windows?
221
- return nil unless IO.select([STDIN], nil, nil, 0.005)
222
- STDIN.read_nonblock(1) rescue nil
238
+ b = windows_queue_pop_with_timeout(0.005)
239
+ b.nil? ? nil : b.chr(Encoding::BINARY)
223
240
  else
224
241
  STDIN.read_nonblock(1) rescue nil
225
242
  end
226
243
  end
227
244
 
228
245
  # SGRマウスシーケンスの後続バイトを読み取る。
229
- # \e[<Btn;Col;RowM/m は最大 15 バイト程度になるため、
230
- # Windows Console のイベントキューにバイトが積まれるまで 20ms 待つ。
246
+ # Windows: キューから取得(20ms タイムアウト)。
231
247
  def read_next_mouse_byte
232
248
  if windows?
233
- return nil unless IO.select([STDIN], nil, nil, 0.020)
234
- STDIN.read_nonblock(1) rescue nil
249
+ b = windows_queue_pop_with_timeout(0.020)
250
+ b.nil? ? nil : b.chr(Encoding::BINARY)
235
251
  else
236
252
  STDIN.read_nonblock(1) rescue nil
237
253
  end
238
254
  end
239
255
 
256
+ # Windows 専用: キューから1文字(マルチバイト対応)を組み立てる。
257
+ # first_byte_int は getbyte が返す Integer。
258
+ def windows_build_char(first_byte_int)
259
+ remaining = case first_byte_int
260
+ when 0x00..0x7F then 0
261
+ when 0xC0..0xDF then 1
262
+ when 0xE0..0xEF then 2
263
+ when 0xF0..0xF7 then 3
264
+ else 0
265
+ end
266
+ if remaining == 0
267
+ first_byte_int.chr(Encoding::UTF_8)
268
+ else
269
+ buf = first_byte_int.chr(Encoding::BINARY)
270
+ remaining.times do
271
+ b = windows_queue_pop_with_timeout(0.005)
272
+ return nil if b.nil?
273
+ buf << b.chr(Encoding::BINARY)
274
+ end
275
+ result = buf.force_encoding(Encoding::UTF_8)
276
+ result.valid_encoding? ? result : nil
277
+ end
278
+ end
279
+
280
+ # Windows 専用: @input_queue からタイムアウト付きで Integer バイトを取り出す。
281
+ def windows_queue_pop_with_timeout(timeout_sec)
282
+ deadline = Time.now + timeout_sec
283
+ loop do
284
+ begin
285
+ return @input_queue.pop(true)
286
+ rescue ThreadError
287
+ return nil if Time.now >= deadline
288
+ sleep 0.001
289
+ end
290
+ end
291
+ end
292
+
240
293
  def cleanup_terminal
294
+ # Windows 入力スレッドを停止(raw! 解除前に kill)
295
+ if windows? && @input_thread
296
+ @input_thread.kill rescue nil
297
+ @input_thread.join(0.5) rescue nil
298
+ @input_thread = nil
299
+ end
300
+
241
301
  # rawモードを解除
242
302
  if STDIN.tty?
243
303
  STDIN.cooked!
@@ -251,8 +311,9 @@ module Rufio
251
311
  end
252
312
  STDOUT.flush
253
313
 
254
- system('tput rmcup') # normal screen
255
- system('tput cnorm') # cursor normal
314
+ print "\e[?25h" # cursor visible
315
+ print "\e[?1049l" # restore normal screen buffer
316
+ STDOUT.flush
256
317
  puts ConfigLoader.message('app.terminated')
257
318
  end
258
319
 
@@ -369,9 +430,9 @@ module Rufio
369
430
  needs_redraw = true
370
431
  end
371
432
 
372
- # 非同期シンタックスハイライト完了チェック(バックグラウンドスレッドからの通知)
373
- if @highlight_updated
374
- @highlight_updated = false
433
+ # 非同期シンタックスハイライト完了チェック(UIRenderer側のフラグを参照)
434
+ if @ui_renderer.highlight_updated?
435
+ @ui_renderer.reset_highlight_updated
375
436
  needs_redraw = true
376
437
  end
377
438
 
@@ -432,25 +493,28 @@ module Rufio
432
493
  private
433
494
 
434
495
  # ノンブロッキング入力処理(ゲームループ用)
435
- # Windows: IO.select(timeout=1ms) で入力確認後 read_nonblock
436
- # timeout=0 だとESCキーを取りこぼす(コンソールの処理タイミング競合)ため
437
- # 1ms の正のタイムアウトを使う。コンソールハンドルでも ConPTY パイプでも動作する。
496
+ # Windows: ブロッキング入力スレッドのキューからノンブロッキングで取得
497
+ # (IO.select の ConPTY シグナル遅延問題を回避)
438
498
  # Unix: IO.select(timeout=0) で入力確認後 read_nonblock
439
499
  def handle_input_nonblocking
440
- # 入力バイトを1つ読み取る
441
500
  if windows?
442
- # Windows: timeout=0 ではESCキーを取りこぼすため 1ms タイムアウトを使用
443
- return false unless IO.select([STDIN], nil, nil, 0.001)
501
+ # Windows: ブロッキング入力スレッドが積んだキューから取得
502
+ raw_byte = begin
503
+ @input_queue.pop(true)
504
+ rescue ThreadError
505
+ return false
506
+ end
507
+ input = windows_build_char(raw_byte)
508
+ return false unless input
444
509
  else
445
510
  # Unix: 0msタイムアウトで即座にチェック(30FPS = 33.33ms/frame)
446
511
  return false unless IO.select([STDIN], nil, nil, 0)
447
- end
448
-
449
- begin
450
- input = @multibyte_reader.read_char
451
- return false if input.nil?
452
- rescue Errno::ENOTTY, Errno::ENODEV
453
- return false
512
+ begin
513
+ input = @multibyte_reader.read_char
514
+ return false if input.nil?
515
+ rescue Errno::ENOTTY, Errno::ENODEV
516
+ return false
517
+ end
454
518
  end
455
519
 
456
520
  # コマンドモードがアクティブな場合は、エスケープシーケンス処理をスキップ
@@ -492,15 +556,19 @@ module Rufio
492
556
  when 'C' then 'l' # Right arrow
493
557
  when 'D' then 'h' # Left arrow
494
558
  when 'Z' then handle_shift_tab; return true # Shift+Tab
495
- else "\e" # ESCキー(そのまま保持)
559
+ else
560
+ # 未知の CSI シーケンス(\e[27;1u 等): 残りバイトを読み捨てて
561
+ # パイプを汚染しないようにする
562
+ drain_csi_sequence(third_char)
563
+ "\e"
496
564
  end
497
565
  else
498
566
  input = "\e" # ESCキー(そのまま保持)
499
567
  end
500
568
  end
501
569
 
502
- # TabキーはFilesモードの時のみブックマーク循環移動
503
- if input == "\t" && @tab_mode_manager.current_mode == :files
570
+ # TabキーはFilesモードの時のみブックマーク循環移動(ジョブモード中は無効)
571
+ if input == "\t" && @tab_mode_manager.current_mode == :files && !@in_job_mode
504
572
  handle_tab_key
505
573
  return true
506
574
  end
@@ -703,6 +771,8 @@ module Rufio
703
771
  def activate_command_mode
704
772
  @command_mode_active = true
705
773
  @command_input = ""
774
+ print "\e[?25h" # カーソルを表示(テキスト入力中)
775
+ STDOUT.flush
706
776
  # 閲覧中ディレクトリをコマンドモードに通知(ローカルスクリプト・Rakefileの検出用)
707
777
  browsing_dir = @directory_listing&.current_path || Dir.pwd
708
778
  @command_mode.update_browsing_directory(browsing_dir)
@@ -712,6 +782,8 @@ module Rufio
712
782
  def deactivate_command_mode
713
783
  @command_mode_active = false
714
784
  @command_input = ""
785
+ print "\e[?25l" # カーソルを非表示(通常のファイラー表示に戻る)
786
+ STDOUT.flush
715
787
  # オーバーレイをクリア
716
788
  @screen&.clear_overlay if @screen&.overlay_enabled?
717
789
  end
@@ -923,8 +995,7 @@ module Rufio
923
995
  @job_manager = job_manager
924
996
  @notification_manager = notification_manager
925
997
  @in_job_mode = true
926
- # 画面を一度クリアしてレンダラーをリセット
927
- print "\e[2J\e[H"
998
+ # レンダラーをリセット(print "\e[2J\e[H" は renderer.clear に含まれる)
928
999
  @renderer.clear if @renderer
929
1000
  # 再描画フラグを立てる
930
1001
  @job_mode_needs_redraw = true
@@ -937,7 +1008,6 @@ module Rufio
937
1008
  @job_manager = nil
938
1009
  # バッファベースの全画面再描画を使用
939
1010
  update_screen_size
940
- print "\e[2J\e[H"
941
1011
  if @screen && @renderer
942
1012
  @renderer.clear
943
1013
  @screen.clear
@@ -1135,6 +1205,16 @@ module Rufio
1135
1205
  })
1136
1206
  end
1137
1207
 
1208
+ # 未知の CSI シーケンス(\e[X...)の残りバイトを終端アルファベットまで読み捨てる。
1209
+ # Windows Terminal が \e[27;1u 等を送った場合のパイプ汚染を防ぐ。
1210
+ def drain_csi_sequence(first_char)
1211
+ return if first_char.nil? || first_char =~ /[A-Za-z]/
1212
+ 10.times do
1213
+ ch = read_next_input_byte
1214
+ break if ch.nil? || ch =~ /[A-Za-z]/
1215
+ end
1216
+ end
1217
+
1138
1218
  end
1139
1219
  end
1140
1220
 
@@ -128,7 +128,8 @@ module Rufio
128
128
 
129
129
  if in_job_mode
130
130
  # ジョブモード: フッタ y=0(上部)、コンテンツ y=1〜h-2、統合行 y=h-1(下部)
131
- draw_job_footer_to_buffer(screen, 0, job_manager)
131
+ log_mode = job_mode_instance&.log_mode? || false
132
+ draw_job_footer_to_buffer(screen, 0, job_manager, log_mode: log_mode)
132
133
  draw_job_list_to_buffer(screen, content_height, job_manager, job_mode_instance)
133
134
  draw_mode_tabs_to_buffer(screen, @screen_height - 1)
134
135
  else
@@ -659,6 +660,12 @@ module Rufio
659
660
  def draw_job_list_to_buffer(screen, height, job_manager, job_mode_instance)
660
661
  return unless job_manager
661
662
 
663
+ # ログモード中は選択ジョブのログを表示
664
+ if job_mode_instance&.log_mode?
665
+ draw_job_log_to_buffer(screen, height, job_mode_instance.selected_job)
666
+ return
667
+ end
668
+
662
669
  jobs = job_manager.jobs
663
670
  selected_index = job_mode_instance&.selected_index || 0
664
671
 
@@ -674,6 +681,24 @@ module Rufio
674
681
  end
675
682
  end
676
683
 
684
+ def draw_job_log_to_buffer(screen, height, job)
685
+ unless job
686
+ screen.put_string(0, CONTENT_START_LINE, 'No job selected'.ljust(@screen_width), fg: "\e[90m")
687
+ return
688
+ end
689
+
690
+ log_lines = job.logs || []
691
+ title = "=== Log: #{job.name} ==="
692
+ screen.put_string(0, CONTENT_START_LINE, title.ljust(@screen_width), fg: "\e[1;36m")
693
+
694
+ (0...height - 1).each do |i|
695
+ line_num = i + CONTENT_START_LINE + 1
696
+ line = log_lines[i] || ''
697
+ line = line[0...@screen_width].ljust(@screen_width)
698
+ screen.put_string(0, line_num, line, fg: "\e[37m")
699
+ end
700
+ end
701
+
677
702
  def draw_job_line_to_buffer(screen, job, is_selected, y)
678
703
  icon = job.status_icon
679
704
  name = job.name
@@ -712,9 +737,13 @@ module Rufio
712
737
  end
713
738
  end
714
739
 
715
- def draw_job_footer_to_buffer(screen, y, job_manager)
740
+ def draw_job_footer_to_buffer(screen, y, job_manager, log_mode: false)
716
741
  job_count = job_manager&.job_count || 0
717
- help_text = "[Space] View Log | [x] Cancel | [Tab] Switch Mode | Jobs: #{job_count}"
742
+ help_text = if log_mode
743
+ "[ESC] Close Log | Jobs: #{job_count}"
744
+ else
745
+ "[Space] View Log | [x] Cancel | Jobs: #{job_count}"
746
+ end
718
747
  footer_content = help_text.center(@screen_width)[0...@screen_width]
719
748
 
720
749
  footer_content.each_char.with_index do |char, x|
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '0.91.0'
4
+ VERSION = '1.0.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rufio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.91.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-12 00:00:00.000000000 Z
11
+ date: 2026-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: io-console