rufio 0.62.0 → 0.63.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: 2db20061e61dc6e74d9a106ca35f66e2f20f0d3dfd5394664823a03ddc8b029d
4
- data.tar.gz: caaf0e05574780ca8dc4c86afae1c617e584ae8b56f3654e1744541127172a66
3
+ metadata.gz: 903b51d0ee611d29ad7ea8961e7948b9dceae16b6d04280c28252149ae2fdd20
4
+ data.tar.gz: 287941ec941e8200c98a881c3ce6de2f7cc7ce3b13e63ba17875ff2222c92bc8
5
5
  SHA512:
6
- metadata.gz: 21484e20e462d20f1cd4a547e3516bd80ad986fe810724c0c2f9300504ebc3c04ac512fdee457a7f839c9f8d04bf375df337896399fdfff047cd48faa7eaf6f2
7
- data.tar.gz: 7845c80320bcd88d0540ee2aa38603ad1f9b45ee88f533586d9a5e7fe53b51bff919ff9359fb725873fc430938c49b143498ae7a55fb4f9dd832a99c2a93febe
6
+ metadata.gz: ab00eea888ea729262a4b8c787ecdf43814afe0c573fdc2624597b014027836ae81da4342b413024a10ba6f3212faf5cee1f987af45357fc99134f93d3425314
7
+ data.tar.gz: c665a171a03ab2ebe1941d7b38eeac86ccbd1552c3ee5a7e8cf252fa20293470399cf4cf1ecdeacba8d0d6fb251d5cd4b87ae74a4b4e924814d6821c34d99857
data/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.63.0] - 2026-02-01
11
+
12
+ ### Added
13
+ - **Tab Mode Switching**: Seamless mode navigation via `Tab` / `Shift+Tab` keys
14
+ - New `TabModeManager` class for unified mode management
15
+ - Four modes: Files, Logs, Jobs, Help
16
+ - Mode tabs displayed in header row 2 (cyan highlight for current mode)
17
+ - Circular navigation: Files → Logs → Jobs → Help → Files
18
+
19
+ ### Changed
20
+ - **Header UI Overhaul**: Two-row header layout
21
+ - Row 1: Application name and current path
22
+ - Row 2: Interactive mode tabs
23
+ - Header icon changed from 📁 to 💎 (representing Ruby gem)
24
+ - **Job Mode Integration**: Unified buffer-based rendering
25
+ - `draw_job_list_to_buffer`, `draw_job_line_to_buffer`, `draw_job_footer_to_buffer`
26
+ - Consistent rendering pipeline with other modes
27
+ - **Help/Logs Mode Improvements**:
28
+ - Parent directory (`..`) hidden in Help and Logs modes
29
+ - Directory navigation disabled in these modes
30
+
31
+ ### Technical Details
32
+ - **New Files**: `lib/rufio/tab_mode_manager.rb`, `test/test_tab_mode.rb`
33
+ - **Modified Files**: `lib/rufio.rb`, `lib/rufio/keybind_handler.rb`, `lib/rufio/terminal_ui.rb`
34
+ - **Layout Constants**: `HEADER_HEIGHT` changed from 1 to 2
35
+
10
36
  ## [0.62.0] - 2026-01-31
11
37
 
12
38
  ### Changed
data/README.md CHANGED
@@ -133,6 +133,8 @@ rufio /path/to # Launch in specified directory
133
133
  | `J` | Job mode |
134
134
  | `L` | View execution logs |
135
135
  | `?` | Help |
136
+ | `Tab` | Switch mode (Files → Logs → Jobs → Help) |
137
+ | `Shift+Tab` | Switch mode (reverse) |
136
138
 
137
139
  ## Command Mode
138
140
 
@@ -491,6 +491,9 @@ module Rufio
491
491
  return false unless entry
492
492
 
493
493
  if entry[:type] == 'directory'
494
+ # ヘルプモードとLogsモードではディレクトリ移動を禁止
495
+ return false if @in_help_mode || @in_log_viewer_mode
496
+
494
497
  result = @directory_listing.navigate_to(entry[:name])
495
498
  if result
496
499
  @current_index = 0 # select first entry in new directory
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # タブモード管理クラス
5
+ # 上部メニューの2段目に表示するモードタブを管理する
6
+ class TabModeManager
7
+ MODES = %i[files logs jobs help].freeze
8
+
9
+ MODE_LABELS = {
10
+ files: 'Files',
11
+ logs: 'Logs',
12
+ jobs: 'Jobs',
13
+ help: 'Help'
14
+ }.freeze
15
+
16
+ attr_reader :current_mode
17
+
18
+ def initialize
19
+ @current_mode = :files
20
+ @mode_change_callback = nil
21
+ end
22
+
23
+ # 利用可能なモード一覧
24
+ def available_modes
25
+ MODES
26
+ end
27
+
28
+ # モードのラベル一覧
29
+ def mode_labels
30
+ MODE_LABELS
31
+ end
32
+
33
+ # 次のモードに切り替え
34
+ def next_mode
35
+ current_index = MODES.index(@current_mode)
36
+ new_index = (current_index + 1) % MODES.length
37
+ switch_to(MODES[new_index])
38
+ end
39
+
40
+ # 前のモードに切り替え
41
+ def previous_mode
42
+ current_index = MODES.index(@current_mode)
43
+ new_index = (current_index - 1) % MODES.length
44
+ switch_to(MODES[new_index])
45
+ end
46
+
47
+ # 特定のモードに切り替え
48
+ def switch_to(mode)
49
+ return unless MODES.include?(mode)
50
+ return if @current_mode == mode
51
+
52
+ @current_mode = mode
53
+ @mode_change_callback&.call(mode)
54
+ end
55
+
56
+ # 現在のモード情報を取得
57
+ def current_mode_info
58
+ {
59
+ mode: @current_mode,
60
+ label: MODE_LABELS[@current_mode],
61
+ index: MODES.index(@current_mode)
62
+ }
63
+ end
64
+
65
+ # モード変更時のコールバックを設定
66
+ def on_mode_change(&block)
67
+ @mode_change_callback = block
68
+ end
69
+
70
+ # タブ行をレンダリング
71
+ # @param width [Integer] 画面幅
72
+ # @return [String] ANSIエスケープシーケンスを含むタブ行
73
+ def render_tab_line(width)
74
+ tabs = MODES.map do |mode|
75
+ label = MODE_LABELS[mode]
76
+ if mode == @current_mode
77
+ # 現在のモード: シアン背景 + 黒文字 + 太字で目立たせる
78
+ "\e[46m\e[30m\e[1m #{label} \e[0m"
79
+ else
80
+ # 非選択モード: 暗めの色
81
+ "\e[90m #{label} \e[0m"
82
+ end
83
+ end
84
+
85
+ tab_content = tabs.join("\e[90m│\e[0m")
86
+
87
+ # 幅に合わせてパディング
88
+ # ANSIエスケープシーケンスを除いた実際の表示幅を計算
89
+ display_width = strip_ansi(tab_content).length
90
+ padding = width - display_width
91
+
92
+ if padding > 0
93
+ tab_content + ' ' * padding
94
+ else
95
+ tab_content[0...width]
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ # ANSIエスケープシーケンスを除去して実際の表示幅を取得
102
+ def strip_ansi(str)
103
+ str.gsub(/\e\[[0-9;]*m/, '')
104
+ end
105
+ end
106
+ end
@@ -6,9 +6,9 @@ require_relative 'text_utils'
6
6
  module Rufio
7
7
  class TerminalUI
8
8
  # Layout constants
9
- HEADER_HEIGHT = 1 # Header占有行数
9
+ HEADER_HEIGHT = 2 # Header占有行数(2段目のモードタブを含む)
10
10
  FOOTER_HEIGHT = 1 # Footer占有行数(ブックマーク一覧 + ステータス情報)
11
- HEADER_FOOTER_MARGIN = 3 # Header + Footer分のマージン
11
+ HEADER_FOOTER_MARGIN = 3 # Header(2行) + Footer(1行)分のマージン
12
12
 
13
13
  # Panel layout ratios
14
14
  LEFT_PANEL_RATIO = 0.5 # 左パネルの幅比率
@@ -30,7 +30,7 @@ module Rufio
30
30
  GIGABYTE = MEGABYTE * 1024
31
31
 
32
32
  # Line offsets
33
- CONTENT_START_LINE = 2 # コンテンツ開始行(ヘッダー1行スキップ)
33
+ CONTENT_START_LINE = 2 # コンテンツ開始行(ヘッダー2行: Y=0, Y=1
34
34
 
35
35
  def initialize(test_mode: false)
36
36
  console = IO.console
@@ -74,6 +74,9 @@ module Rufio
74
74
  # Command execution lamp (footer indicator)
75
75
  @completion_lamp_message = nil
76
76
  @completion_lamp_time = nil
77
+
78
+ # Tab mode manager
79
+ @tab_mode_manager = TabModeManager.new
77
80
  end
78
81
 
79
82
  def start(directory_listing, keybind_handler, file_preview, background_executor = nil)
@@ -371,35 +374,34 @@ module Rufio
371
374
 
372
375
  # Phase 3: Screenバッファに描画する新しいメソッド
373
376
  def draw_screen_to_buffer(screen, notification_message = nil, fps = nil)
374
- # ジョブモードの場合は既存の描画メソッドを使用
375
- if @in_job_mode
376
- if @job_mode_needs_redraw
377
- draw_job_mode_screen
378
- @job_mode_needs_redraw = false
379
- end
380
- return
381
- end
382
-
383
- # header (1 line) - y=0
377
+ # header (2 lines) - y=0, y=1(全モード共通)
384
378
  draw_header_to_buffer(screen, 0)
385
-
386
- # main content (left: directory list, right: preview)
387
- entries = get_display_entries
388
- selected_entry = entries[@keybind_handler.current_index]
379
+ draw_mode_tabs_to_buffer(screen, 1)
389
380
 
390
381
  # calculate height with header and footer margin
391
382
  content_height = @screen_height - HEADER_FOOTER_MARGIN
392
- left_width = (@screen_width * LEFT_PANEL_RATIO).to_i
393
- right_width = @screen_width - left_width
394
383
 
395
- # adjust so right panel doesn't overflow into left panel
396
- right_width = @screen_width - left_width if left_width + right_width > @screen_width
384
+ if @in_job_mode
385
+ # ジョブモード: ジョブ一覧を表示
386
+ draw_job_list_to_buffer(screen, content_height)
387
+ draw_job_footer_to_buffer(screen, @screen_height - 1)
388
+ else
389
+ # 通常モード: ファイル一覧とプレビューを表示
390
+ entries = get_display_entries
391
+ selected_entry = entries[@keybind_handler.current_index]
397
392
 
398
- draw_directory_list_to_buffer(screen, entries, left_width, content_height)
399
- draw_file_preview_to_buffer(screen, selected_entry, right_width, content_height, left_width)
393
+ left_width = (@screen_width * LEFT_PANEL_RATIO).to_i
394
+ right_width = @screen_width - left_width
400
395
 
401
- # footer
402
- draw_footer_to_buffer(screen, @screen_height - 1, fps)
396
+ # adjust so right panel doesn't overflow into left panel
397
+ right_width = @screen_width - left_width if left_width + right_width > @screen_width
398
+
399
+ draw_directory_list_to_buffer(screen, entries, left_width, content_height)
400
+ draw_file_preview_to_buffer(screen, selected_entry, right_width, content_height, left_width)
401
+
402
+ # footer
403
+ draw_footer_to_buffer(screen, @screen_height - 1, fps)
404
+ end
403
405
 
404
406
  # 通知メッセージがある場合は表示
405
407
  if notification_message
@@ -410,10 +412,80 @@ module Rufio
410
412
  end
411
413
  screen.put_string(0, notification_line, message_display.ljust(@screen_width), fg: "\e[7m")
412
414
  end
415
+ end
416
+
417
+ # ジョブ一覧をバッファに描画
418
+ def draw_job_list_to_buffer(screen, height)
419
+ return unless @job_manager
420
+
421
+ jobs = @job_manager.jobs
422
+ selected_index = @job_mode_instance&.selected_index || 0
423
+
424
+ (0...height).each do |i|
425
+ line_num = i + CONTENT_START_LINE
426
+
427
+ if i < jobs.length
428
+ job = jobs[i]
429
+ draw_job_line_to_buffer(screen, job, i == selected_index, line_num)
430
+ else
431
+ # 空行
432
+ screen.put_string(0, line_num, ' ' * @screen_width)
433
+ end
434
+ end
435
+ end
413
436
 
414
- # コマンドモードがアクティブな場合はフローティングウィンドウを描画
415
- # Phase 4: 暫定的に既存のメソッドを使用
416
- # Phase 5でScreenバッファ統合予定
437
+ # ジョブ行をバッファに描画
438
+ def draw_job_line_to_buffer(screen, job, is_selected, y)
439
+ icon = job.status_icon
440
+ name = job.name
441
+ path = "(#{job.path})"
442
+ duration = job.formatted_duration
443
+ duration_text = duration.empty? ? "" : "[#{duration}]"
444
+
445
+ status_text = case job.status
446
+ when :running then "Running"
447
+ when :completed then "Done"
448
+ when :failed then "Failed"
449
+ when :waiting then "Waiting"
450
+ when :cancelled then "Cancelled"
451
+ else ""
452
+ end
453
+
454
+ # ステータスに応じた色
455
+ status_color = case job.status
456
+ when :running then "\e[33m" # Yellow
457
+ when :completed then "\e[32m" # Green
458
+ when :failed then "\e[31m" # Red
459
+ else "\e[37m" # White
460
+ end
461
+
462
+ # 行を構築
463
+ line_content = "#{icon} #{name} #{path}".ljust(40)
464
+ line_content += "#{duration_text.ljust(12)} #{status_text}"
465
+ line_content = line_content[0...@screen_width].ljust(@screen_width)
466
+
467
+ if is_selected
468
+ # 選択中: 反転表示
469
+ line_content.each_char.with_index do |char, x|
470
+ screen.put(x, y, char, fg: "\e[30m", bg: "\e[47m")
471
+ end
472
+ else
473
+ # 非選択: ステータス色
474
+ line_content.each_char.with_index do |char, x|
475
+ screen.put(x, y, char, fg: status_color)
476
+ end
477
+ end
478
+ end
479
+
480
+ # ジョブモード用フッターをバッファに描画
481
+ def draw_job_footer_to_buffer(screen, y)
482
+ job_count = @job_manager&.job_count || 0
483
+ help_text = "[Space] View Log | [x] Cancel | [Tab] Switch Mode | Jobs: #{job_count}"
484
+ footer_content = help_text.center(@screen_width)[0...@screen_width]
485
+
486
+ footer_content.each_char.with_index do |char, x|
487
+ screen.put(x, y, char, fg: "\e[30m", bg: "\e[47m")
488
+ end
417
489
  end
418
490
 
419
491
  def draw_screen_with_notification(notification_message)
@@ -436,7 +508,7 @@ module Rufio
436
508
  # Phase 3: Screenバッファにヘッダーを描画
437
509
  def draw_header_to_buffer(screen, y)
438
510
  current_path = @directory_listing.current_path
439
- header = "📁 rufio - #{current_path}"
511
+ header = "💎 rufio - #{current_path}"
440
512
 
441
513
  # Add help mode indicator if in help mode
442
514
  if @keybind_handler.help_mode?
@@ -455,23 +527,81 @@ module Rufio
455
527
  # prioritize showing help mode indicator
456
528
  help_text = " [Help Mode - Press ESC to exit]"
457
529
  base_length = @screen_width - help_text.length - FILTER_TEXT_RESERVED
458
- header = "📁 rufio - ...#{current_path[-base_length..-1]}#{help_text}"
530
+ header = "💎 rufio - ...#{current_path[-base_length..-1]}#{help_text}"
459
531
  elsif @keybind_handler.filter_active?
460
532
  # prioritize showing filter when active
461
533
  filter_text = " [Filter: #{@keybind_handler.filter_query}]"
462
534
  base_length = @screen_width - filter_text.length - FILTER_TEXT_RESERVED
463
- header = "📁 rufio - ...#{current_path[-base_length..-1]}#{filter_text}"
535
+ header = "💎 rufio - ...#{current_path[-base_length..-1]}#{filter_text}"
464
536
  else
465
- header = "📁 rufio - ...#{current_path[-(@screen_width - FILTER_TEXT_RESERVED)..-1]}"
537
+ header = "💎 rufio - ...#{current_path[-(@screen_width - FILTER_TEXT_RESERVED)..-1]}"
466
538
  end
467
539
  end
468
540
 
469
541
  screen.put_string(0, y, header.ljust(@screen_width), fg: "\e[7m")
470
542
  end
471
543
 
544
+ # Phase 3: Screenバッファにモードタブを描画
545
+ def draw_mode_tabs_to_buffer(screen, y)
546
+ # タブモードマネージャの状態を同期
547
+ sync_tab_mode_with_keybind_handler
548
+
549
+ current_x = 0
550
+ modes = @tab_mode_manager.available_modes
551
+ labels = @tab_mode_manager.mode_labels
552
+ current_mode = @tab_mode_manager.current_mode
553
+
554
+ modes.each_with_index do |mode, index|
555
+ label = " #{labels[mode]} "
556
+
557
+ if mode == current_mode
558
+ # 現在のモード: シアン背景 + 黒文字 + 太字
559
+ label.each_char do |char|
560
+ screen.put(current_x, y, char, fg: "\e[30m\e[1m", bg: "\e[46m")
561
+ current_x += 1
562
+ end
563
+ else
564
+ # 非選択モード: グレー文字
565
+ label.each_char do |char|
566
+ screen.put(current_x, y, char, fg: "\e[90m")
567
+ current_x += 1
568
+ end
569
+ end
570
+
571
+ # 区切り線(最後のモード以外)
572
+ if index < modes.length - 1
573
+ screen.put(current_x, y, '│', fg: "\e[90m")
574
+ current_x += 1
575
+ end
576
+ end
577
+
578
+ # 残りをスペースで埋める
579
+ while current_x < @screen_width
580
+ screen.put(current_x, y, ' ')
581
+ current_x += 1
582
+ end
583
+ end
584
+
585
+ # キーバインドハンドラの状態とタブモードを同期
586
+ def sync_tab_mode_with_keybind_handler
587
+ return unless @keybind_handler
588
+
589
+ current_mode = if @keybind_handler.help_mode?
590
+ :help
591
+ elsif @keybind_handler.log_viewer_mode?
592
+ :logs
593
+ elsif @keybind_handler.in_job_mode?
594
+ :jobs
595
+ else
596
+ :files
597
+ end
598
+
599
+ @tab_mode_manager.switch_to(current_mode) if @tab_mode_manager.current_mode != current_mode
600
+ end
601
+
472
602
  def draw_header
473
603
  current_path = @directory_listing.current_path
474
- header = "📁 rufio - #{current_path}"
604
+ header = "💎 rufio - #{current_path}"
475
605
 
476
606
  # Add help mode indicator if in help mode
477
607
  if @keybind_handler.help_mode?
@@ -490,14 +620,14 @@ module Rufio
490
620
  # prioritize showing help mode indicator
491
621
  help_text = " [Help Mode - Press ESC to exit]"
492
622
  base_length = @screen_width - help_text.length - FILTER_TEXT_RESERVED
493
- header = "📁 rufio - ...#{current_path[-base_length..-1]}#{help_text}"
623
+ header = "💎 rufio - ...#{current_path[-base_length..-1]}#{help_text}"
494
624
  elsif @keybind_handler.filter_active?
495
625
  # prioritize showing filter when active
496
626
  filter_text = " [Filter: #{@keybind_handler.filter_query}]"
497
627
  base_length = @screen_width - filter_text.length - FILTER_TEXT_RESERVED
498
- header = "📁 rufio - ...#{current_path[-base_length..-1]}#{filter_text}"
628
+ header = "💎 rufio - ...#{current_path[-base_length..-1]}#{filter_text}"
499
629
  else
500
- header = "📁 rufio - ...#{current_path[-(@screen_width - FILTER_TEXT_RESERVED)..-1]}"
630
+ header = "💎 rufio - ...#{current_path[-(@screen_width - FILTER_TEXT_RESERVED)..-1]}"
501
631
  end
502
632
  end
503
633
 
@@ -869,13 +999,20 @@ module Rufio
869
999
 
870
1000
 
871
1001
  def get_display_entries
872
- if @keybind_handler.filter_active?
873
- # Get filtered entries from keybind_handler
874
- all_entries = @directory_listing.list_entries
875
- query = @keybind_handler.filter_query.downcase
876
- query.empty? ? all_entries : all_entries.select { |entry| entry[:name].downcase.include?(query) }
1002
+ entries = if @keybind_handler.filter_active?
1003
+ # Get filtered entries from keybind_handler
1004
+ all_entries = @directory_listing.list_entries
1005
+ query = @keybind_handler.filter_query.downcase
1006
+ query.empty? ? all_entries : all_entries.select { |entry| entry[:name].downcase.include?(query) }
1007
+ else
1008
+ @directory_listing.list_entries
1009
+ end
1010
+
1011
+ # ヘルプモードとLogsモードでは..を非表示にする
1012
+ if @keybind_handler.help_mode? || @keybind_handler.log_viewer_mode?
1013
+ entries.reject { |entry| entry[:name] == '..' }
877
1014
  else
878
- @directory_listing.list_entries
1015
+ entries
879
1016
  end
880
1017
  end
881
1018
 
@@ -1076,6 +1213,7 @@ module Rufio
1076
1213
  when 'B' then 'j' # Down arrow
1077
1214
  when 'C' then 'l' # Right arrow
1078
1215
  when 'D' then 'h' # Left arrow
1216
+ when 'Z' then handle_shift_tab; return true # Shift+Tab
1079
1217
  else "\e" # ESCキー(そのまま保持)
1080
1218
  end
1081
1219
  else
@@ -1083,6 +1221,12 @@ module Rufio
1083
1221
  end
1084
1222
  end
1085
1223
 
1224
+ # Tabキーでモード切り替え
1225
+ if input == "\t"
1226
+ handle_tab_key
1227
+ return true
1228
+ end
1229
+
1086
1230
  # キーバインドハンドラーに処理を委譲
1087
1231
  result = @keybind_handler.handle_key(input) if input
1088
1232
 
@@ -1156,6 +1300,54 @@ module Rufio
1156
1300
  end
1157
1301
  end
1158
1302
 
1303
+ # Tabキーによるモード切り替え
1304
+ def handle_tab_key
1305
+ @tab_mode_manager.next_mode
1306
+ apply_mode_change(@tab_mode_manager.current_mode)
1307
+ end
1308
+
1309
+ # Shift+Tabによる逆順モード切り替え
1310
+ def handle_shift_tab
1311
+ @tab_mode_manager.previous_mode
1312
+ apply_mode_change(@tab_mode_manager.current_mode)
1313
+ end
1314
+
1315
+ # モード変更を適用
1316
+ def apply_mode_change(mode)
1317
+ case mode
1318
+ when :files
1319
+ # ヘルプモードまたはログビューワモードから戻る
1320
+ if @keybind_handler.help_mode?
1321
+ @keybind_handler.send(:exit_help_mode)
1322
+ elsif @keybind_handler.log_viewer_mode?
1323
+ @keybind_handler.send(:exit_log_viewer_mode)
1324
+ elsif @keybind_handler.in_job_mode?
1325
+ @keybind_handler.send(:exit_job_mode)
1326
+ end
1327
+ when :help
1328
+ # ヘルプモードに入る
1329
+ unless @keybind_handler.help_mode?
1330
+ @keybind_handler.send(:exit_log_viewer_mode) if @keybind_handler.log_viewer_mode?
1331
+ @keybind_handler.send(:exit_job_mode) if @keybind_handler.in_job_mode?
1332
+ @keybind_handler.send(:enter_help_mode)
1333
+ end
1334
+ when :logs
1335
+ # ログビューワモードに入る
1336
+ unless @keybind_handler.log_viewer_mode?
1337
+ @keybind_handler.send(:exit_help_mode) if @keybind_handler.help_mode?
1338
+ @keybind_handler.send(:exit_job_mode) if @keybind_handler.in_job_mode?
1339
+ @keybind_handler.send(:enter_log_viewer_mode)
1340
+ end
1341
+ when :jobs
1342
+ # ジョブモードに入る
1343
+ unless @keybind_handler.in_job_mode?
1344
+ @keybind_handler.send(:exit_help_mode) if @keybind_handler.help_mode?
1345
+ @keybind_handler.send(:exit_log_viewer_mode) if @keybind_handler.log_viewer_mode?
1346
+ @keybind_handler.enter_job_mode
1347
+ end
1348
+ end
1349
+ end
1350
+
1159
1351
  # コマンドモード関連のメソッドは public にする
1160
1352
  public
1161
1353
 
@@ -1414,81 +1606,15 @@ module Rufio
1414
1606
  @job_mode_needs_redraw = true
1415
1607
  end
1416
1608
 
1417
- # ジョブモード画面を描画
1609
+ # ジョブモード画面を描画(バッファベース描画への橋渡し)
1418
1610
  def draw_job_mode_screen
1419
1611
  return unless @in_job_mode && @job_mode_instance && @job_manager
1612
+ return unless @screen && @renderer
1420
1613
 
1421
- # ヘッダー
1422
- job_count = @job_manager.job_count
1423
- header = "Running Jobs (#{job_count})"
1424
- header_line = header.center(@screen_width)
1425
- print "\e[1;1H\e[1;36m#{header_line}\e[0m"
1426
-
1427
- # 区切り線
1428
- separator = "━" * @screen_width
1429
- print "\e[2;1H\e[36m#{separator}\e[0m"
1430
-
1431
- # ジョブ一覧
1432
- jobs = @job_manager.jobs
1433
- selected_index = @job_mode_instance.selected_index
1434
-
1435
- jobs.each_with_index do |job, i|
1436
- line_num = i + 3
1437
- break if line_num >= @screen_height - 2
1438
-
1439
- # ステータスアイコン
1440
- icon = job.status_icon
1441
- icon_color = case job.status
1442
- when :running then "\e[33m" # Yellow
1443
- when :completed then "\e[32m" # Green
1444
- when :failed then "\e[31m" # Red
1445
- else "\e[37m" # White
1446
- end
1447
-
1448
- # ジョブ名とパス
1449
- name = job.name
1450
- path = "(#{job.path})"
1451
- duration = job.formatted_duration
1452
- duration_text = duration.empty? ? "" : "[#{duration}]"
1453
-
1454
- # ステータステキスト
1455
- status_text = case job.status
1456
- when :running then "Running"
1457
- when :completed then "Done"
1458
- when :failed then "Failed"
1459
- when :waiting then "Waiting"
1460
- when :cancelled then "Cancelled"
1461
- else ""
1462
- end
1463
-
1464
- # 行を構築
1465
- line_content = "#{icon} #{name} #{path}".ljust(40)
1466
- line_content += "#{duration_text.ljust(12)} #{status_text}"
1467
- line_content = line_content[0...@screen_width - 1].ljust(@screen_width - 1)
1468
-
1469
- # 選択状態の場合はハイライト
1470
- if i == selected_index
1471
- print "\e[#{line_num};1H\e[7m#{icon_color}#{line_content}\e[0m"
1472
- else
1473
- print "\e[#{line_num};1H#{icon_color}#{line_content}\e[0m"
1474
- end
1475
- end
1476
-
1477
- # 空行をクリア
1478
- ((jobs.length + 3)...(@screen_height - 2)).each do |line_num|
1479
- print "\e[#{line_num};1H#{' ' * @screen_width}"
1480
- end
1481
-
1482
- # フッター
1483
- footer_line = @screen_height - 1
1484
- footer_separator = "━" * @screen_width
1485
- print "\e[#{footer_line};1H\e[36m#{footer_separator}\e[0m"
1486
-
1487
- # ヘルプライン
1488
- help_line = @screen_height
1489
- help_text = "[Space] View Log | [x] Cancel | [Esc] Back to Files"
1490
- help_content = help_text.center(@screen_width)
1491
- print "\e[#{help_line};1H\e[7m#{help_content}\e[0m"
1614
+ # バッファベースの描画を使用
1615
+ draw_screen_to_buffer(@screen, nil, nil)
1616
+ @renderer.render(@screen)
1617
+ print "\e[#{@screen_height};#{@screen_width}H"
1492
1618
 
1493
1619
  STDOUT.flush
1494
1620
  @job_mode_needs_redraw = false
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.62.0'
4
+ VERSION = '0.63.0'
5
5
  end
data/lib/rufio.rb CHANGED
@@ -43,6 +43,7 @@ require_relative "rufio/async_scanner_fiber"
43
43
  require_relative "rufio/parallel_scanner"
44
44
  require_relative "rufio/screen"
45
45
  require_relative "rufio/renderer"
46
+ require_relative "rufio/tab_mode_manager"
46
47
 
47
48
  # ジョブ管理システム
48
49
  require_relative "rufio/task_status"
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.62.0
4
+ version: 0.63.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-01-31 00:00:00.000000000 Z
11
+ date: 2026-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: io-console
@@ -189,6 +189,7 @@ files:
189
189
  - lib/rufio/script_runner.rb
190
190
  - lib/rufio/selection_manager.rb
191
191
  - lib/rufio/shell_command_completion.rb
192
+ - lib/rufio/tab_mode_manager.rb
192
193
  - lib/rufio/task_status.rb
193
194
  - lib/rufio/terminal_ui.rb
194
195
  - lib/rufio/text_utils.rb