rufio 0.50.0 → 0.60.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.
@@ -44,14 +44,6 @@ module Rufio
44
44
  @bookmark_manager = BookmarkManager.new(Bookmark.new, @dialog_renderer)
45
45
  @zoxide_integration = ZoxideIntegration.new(@dialog_renderer)
46
46
 
47
- # Project mode
48
- log_dir = File.expand_path('~/.config/rufio/logs')
49
- @project_mode = ProjectMode.new(@bookmark_manager.instance_variable_get(:@bookmark), log_dir)
50
- @project_command = ProjectCommand.new(log_dir)
51
- @project_log = ProjectLog.new(log_dir)
52
- @in_project_mode = false
53
- @in_log_mode = false
54
-
55
47
  # Help mode
56
48
  @in_help_mode = false
57
49
  @pre_help_directory = nil
@@ -64,6 +56,15 @@ module Rufio
64
56
  # Preview pane focus and scroll
65
57
  @preview_focused = false
66
58
  @preview_scroll_offset = 0
59
+
60
+ # Job mode
61
+ @notification_manager = NotificationManager.new
62
+ @job_manager = JobManager.new(notification_manager: @notification_manager)
63
+ @job_mode = JobMode.new(job_manager: @job_manager)
64
+
65
+ # Script path manager
66
+ config_file = File.expand_path('~/.config/rufio/config.yml')
67
+ @script_path_manager = File.exist?(config_file) ? ScriptPathManager.new(config_file) : nil
67
68
  end
68
69
 
69
70
  def set_directory_listing(directory_listing)
@@ -93,9 +94,9 @@ module Rufio
93
94
  def handle_key(key)
94
95
  return false unless @directory_listing
95
96
 
96
- # プロジェクトモード中の特別処理
97
- if @in_project_mode
98
- return handle_project_mode_key(key)
97
+ # ジョブモード中の特別処理
98
+ if @job_mode.active?
99
+ return handle_job_mode_key(key)
99
100
  end
100
101
 
101
102
  # プレビューペインフォーカス中の特別処理
@@ -177,10 +178,12 @@ module Rufio
177
178
  copy_selected_to_current
178
179
  when 'x' # x - delete selected files
179
180
  delete_selected_files
180
- when 'P' # P - project mode
181
- enter_project_mode
181
+ when 'J' # J - job mode
182
+ enter_job_mode
182
183
  when 'b' # b - add bookmark
183
184
  add_bookmark
185
+ when 'B' # B - bookmark menu (with script paths)
186
+ show_bookmark_menu
184
187
  when 'z' # z - zoxide history navigation
185
188
  show_zoxide_menu
186
189
  when '0' # 0 - go to start directory
@@ -1408,6 +1411,191 @@ module Rufio
1408
1411
  result
1409
1412
  end
1410
1413
 
1414
+ # ブックマークメニューを表示(スクリプトパス機能を含む)
1415
+ def show_bookmark_menu
1416
+ current_path = @directory_listing&.current_path || Dir.pwd
1417
+
1418
+ menu_items = [
1419
+ '1. Add current dir to bookmarks',
1420
+ '2. Add to script paths',
1421
+ '3. Manage script paths',
1422
+ '4. View bookmarks'
1423
+ ]
1424
+
1425
+ content_lines = [''] + menu_items + ['', '[1-4] Select | [Esc] Cancel']
1426
+
1427
+ title = 'Bookmark Menu'
1428
+ width = 45
1429
+ height = content_lines.length + 4
1430
+ x, y = @dialog_renderer.calculate_center(width, height)
1431
+
1432
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1433
+ border_color: "\e[36m", # Cyan
1434
+ title_color: "\e[1;36m", # Bold cyan
1435
+ content_color: "\e[37m" # White
1436
+ })
1437
+
1438
+ key = STDIN.getch
1439
+ @dialog_renderer.clear_area(x, y, width, height)
1440
+
1441
+ case key
1442
+ when '1'
1443
+ add_bookmark
1444
+ when '2'
1445
+ add_to_script_paths
1446
+ when '3'
1447
+ show_script_paths_manager
1448
+ when '4'
1449
+ # ブックマーク一覧表示(既存機能)
1450
+ @terminal_ui&.refresh_display
1451
+ else
1452
+ @terminal_ui&.refresh_display
1453
+ end
1454
+
1455
+ true
1456
+ end
1457
+
1458
+ # カレントディレクトリをスクリプトパスに追加
1459
+ def add_to_script_paths
1460
+ current_path = @directory_listing&.current_path || Dir.pwd
1461
+
1462
+ unless @script_path_manager
1463
+ # ScriptPathManagerがない場合は作成
1464
+ config_file = File.expand_path('~/.config/rufio/config.yml')
1465
+ FileUtils.mkdir_p(File.dirname(config_file))
1466
+ @script_path_manager = ScriptPathManager.new(config_file)
1467
+ end
1468
+
1469
+ if @script_path_manager.paths.include?(current_path)
1470
+ # 既に登録されている
1471
+ show_notification('Already in paths', current_path, :info)
1472
+ elsif @script_path_manager.add_path(current_path)
1473
+ show_notification('Added to scripts', current_path, :success)
1474
+ else
1475
+ show_notification('Failed to add', current_path, :error)
1476
+ end
1477
+
1478
+ @terminal_ui&.refresh_display
1479
+ true
1480
+ end
1481
+
1482
+ # スクリプトパス管理UIを表示
1483
+ def show_script_paths_manager
1484
+ unless @script_path_manager
1485
+ show_notification('No script paths configured', '', :info)
1486
+ @terminal_ui&.refresh_display
1487
+ return false
1488
+ end
1489
+
1490
+ paths = @script_path_manager.paths
1491
+ if paths.empty?
1492
+ show_notification('No script paths', 'Press B > 2 to add', :info)
1493
+ @terminal_ui&.refresh_display
1494
+ return false
1495
+ end
1496
+
1497
+ selected_index = 0
1498
+
1499
+ loop do
1500
+ # メニューを描画
1501
+ menu_items = paths.each_with_index.map do |path, i|
1502
+ prefix = i == selected_index ? '> ' : ' '
1503
+ "#{prefix}#{i + 1}. #{truncate_path(path, 35)}"
1504
+ end
1505
+
1506
+ content_lines = [''] + menu_items + ['', '[j/k] Move | [d] Delete | [Enter] Jump | [Esc] Back']
1507
+
1508
+ title = 'Script Paths'
1509
+ width = 50
1510
+ height = content_lines.length + 4
1511
+ x, y = @dialog_renderer.calculate_center(width, height)
1512
+
1513
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1514
+ border_color: "\e[35m", # Magenta
1515
+ title_color: "\e[1;35m", # Bold magenta
1516
+ content_color: "\e[37m" # White
1517
+ })
1518
+
1519
+ key = STDIN.getch
1520
+ @dialog_renderer.clear_area(x, y, width, height)
1521
+
1522
+ case key
1523
+ when 'j'
1524
+ selected_index = [selected_index + 1, paths.length - 1].min
1525
+ when 'k'
1526
+ selected_index = [selected_index - 1, 0].max
1527
+ when 'd'
1528
+ # 削除確認
1529
+ path_to_delete = paths[selected_index]
1530
+ if confirm_delete_script_path(path_to_delete)
1531
+ @script_path_manager.remove_path(path_to_delete)
1532
+ paths = @script_path_manager.paths
1533
+ selected_index = [selected_index, paths.length - 1].min
1534
+ break if paths.empty?
1535
+ end
1536
+ when "\r", "\n"
1537
+ # ディレクトリにジャンプ
1538
+ path = paths[selected_index]
1539
+ if Dir.exist?(path)
1540
+ navigate_to_directory(path)
1541
+ end
1542
+ break
1543
+ when "\e"
1544
+ break
1545
+ end
1546
+ end
1547
+
1548
+ @terminal_ui&.refresh_display
1549
+ true
1550
+ end
1551
+
1552
+ # スクリプトパス削除の確認
1553
+ def confirm_delete_script_path(path)
1554
+ content_lines = [
1555
+ '',
1556
+ 'Delete this script path?',
1557
+ '',
1558
+ truncate_path(path, 40),
1559
+ '',
1560
+ '[y] Yes | [n] No'
1561
+ ]
1562
+
1563
+ title = 'Confirm Delete'
1564
+ width = 50
1565
+ height = content_lines.length + 4
1566
+ x, y = @dialog_renderer.calculate_center(width, height)
1567
+
1568
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1569
+ border_color: "\e[31m", # Red
1570
+ title_color: "\e[1;31m", # Bold red
1571
+ content_color: "\e[37m" # White
1572
+ })
1573
+
1574
+ key = STDIN.getch
1575
+ @dialog_renderer.clear_area(x, y, width, height)
1576
+
1577
+ key.downcase == 'y'
1578
+ end
1579
+
1580
+ # 通知を表示
1581
+ def show_notification(title, message, type)
1582
+ return unless @notification_manager
1583
+
1584
+ @notification_manager.add(title, type, duration: 3, exit_code: nil)
1585
+ end
1586
+
1587
+ # パスを短縮表示
1588
+ def truncate_path(path, max_length)
1589
+ return path if path.length <= max_length
1590
+
1591
+ # ホームディレクトリを~に置換
1592
+ display_path = path.sub(Dir.home, '~')
1593
+ return display_path if display_path.length <= max_length
1594
+
1595
+ # 先頭と末尾を残して中間を...に
1596
+ "...#{display_path[-(max_length - 3)..]}"
1597
+ end
1598
+
1411
1599
  # ヘルパーメソッド
1412
1600
  def wait_for_keypress
1413
1601
  print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
@@ -1500,245 +1688,68 @@ module Rufio
1500
1688
  end
1501
1689
  end
1502
1690
 
1503
- # プロジェクトモード中のキー処理
1504
- def handle_project_mode_key(key)
1505
- result = case key
1506
- when "\e" # ESC - ログモードならプロジェクトモードに戻る、そうでなければ終了
1507
- if @in_log_mode
1508
- exit_log_mode
1509
- else
1510
- exit_project_mode
1511
- end
1512
- when ' ' # Space - ブックマークを選択
1513
- if @in_log_mode
1514
- # ログモード中は何もしない
1515
- false
1516
- else
1517
- select_bookmark_in_project_mode
1518
- end
1519
- when 'l' # l - ログディレクトリに移動
1520
- if @in_log_mode
1521
- # すでにログモードの場合は何もしない
1522
- false
1523
- else
1524
- @in_log_mode = true
1525
- @current_index = 0 # ログモードに入るときインデックスをリセット
1526
- @terminal_ui&.enter_log_mode(@project_log) if @terminal_ui
1527
- true
1528
- end
1529
- when 'j' # 下に移動
1530
- move_down_in_project_mode
1531
- when 'k' # 上に移動
1532
- move_up_in_project_mode
1533
- when 'g' # 先頭に移動
1534
- @current_index = 0
1535
- true
1536
- when 'G' # 末尾に移動
1537
- entries = get_project_mode_entries
1538
- @current_index = [entries.length - 1, 0].max
1539
- true
1540
- when ':' # コマンドモード
1541
- activate_project_command_mode
1542
- when 'r' # r - ブックマークをリネーム
1543
- if @in_log_mode
1544
- false
1545
- else
1546
- rename_bookmark_in_project_mode
1547
- end
1548
- when 'd' # d - ブックマークを削除
1549
- if @in_log_mode
1550
- false
1551
- else
1552
- delete_bookmark_in_project_mode
1553
- end
1554
- else
1555
- false
1556
- end
1557
-
1558
- # キー処理後、プロジェクトモードの再描画をトリガー
1559
- @terminal_ui&.trigger_project_mode_redraw if result && @in_project_mode
1560
- result
1561
- end
1562
-
1563
- # プロジェクトモード用のエントリ数取得
1564
- def get_project_mode_entries
1565
- if @in_log_mode
1566
- @project_log.list_log_files
1567
- else
1568
- @project_mode.list_bookmarks
1569
- end
1570
- end
1571
-
1572
- # プロジェクトモード用の下移動
1573
- def move_down_in_project_mode
1574
- entries = get_project_mode_entries
1575
- @current_index = [@current_index + 1, entries.length - 1].min
1576
- true
1577
- end
1578
-
1579
- # プロジェクトモード用の上移動
1580
- def move_up_in_project_mode
1581
- @current_index = [@current_index - 1, 0].max
1582
- true
1583
- end
1584
-
1585
- # プロジェクトモードに入る
1586
- def enter_project_mode
1587
- @project_mode.activate
1588
- @in_project_mode = true
1589
- @current_index = 0 # プロジェクトモードに入るときインデックスをリセット
1590
- @terminal_ui&.set_project_mode(@project_mode, @project_command, @project_log) if @terminal_ui
1691
+ # ヘルプダイアログを表示
1692
+ def show_help_dialog
1693
+ @terminal_ui&.show_help_dialog if @terminal_ui
1591
1694
  true
1592
1695
  end
1593
1696
 
1594
- # プロジェクトモードを終了
1595
- def exit_project_mode
1596
- @project_mode.deactivate
1597
- @in_project_mode = false
1598
- @in_log_mode = false
1599
- @current_index = 0 # 通常モードに戻るときインデックスをリセット
1600
- @terminal_ui&.exit_project_mode if @terminal_ui
1601
- refresh
1602
- true
1603
- end
1697
+ # ジョブモード関連メソッド(publicに戻す)
1698
+ public
1604
1699
 
1605
- # プロジェクトモードでコマンドモードを起動
1606
- def activate_project_command_mode
1607
- # 選択されたプロジェクトがあるかチェック
1608
- if @project_mode.selected_path.nil?
1609
- @terminal_ui&.show_project_not_selected_message if @terminal_ui
1610
- return false
1611
- end
1700
+ # ジョブマネージャを取得
1701
+ attr_reader :job_manager, :job_mode, :notification_manager
1612
1702
 
1613
- @terminal_ui&.activate_project_command_mode(@project_mode, @project_command, @project_log) if @terminal_ui
1703
+ # ジョブモードに入る
1704
+ def enter_job_mode
1705
+ @job_mode.activate
1706
+ @terminal_ui&.set_job_mode(@job_mode, @job_manager, @notification_manager) if @terminal_ui
1614
1707
  true
1615
1708
  end
1616
1709
 
1617
- # ログモードを終了してプロジェクトモードに戻る
1618
- def exit_log_mode
1619
- @in_log_mode = false
1620
- @current_index = 0 # プロジェクトモードに戻るときインデックスをリセット
1621
- @terminal_ui&.exit_log_mode if @terminal_ui
1710
+ # ジョブモードを終了
1711
+ def exit_job_mode
1712
+ @job_mode.deactivate
1713
+ @terminal_ui&.exit_job_mode if @terminal_ui
1714
+ refresh
1622
1715
  true
1623
1716
  end
1624
1717
 
1625
- # プロジェクトモードでブックマークをリネーム
1626
- def rename_bookmark_in_project_mode
1627
- bookmarks = @bookmark_manager.list
1628
- return false if bookmarks.empty? || @current_index >= bookmarks.length
1629
-
1630
- current_bookmark = bookmarks[@current_index]
1631
- old_name = current_bookmark[:name]
1632
-
1633
- # ダイアログレンダラーを使用して入力ダイアログを表示
1634
- new_name = @dialog_renderer.show_input_dialog("Rename: #{old_name}", "Enter new name:", {
1635
- border_color: "\e[33m", # Yellow
1636
- title_color: "\e[1;33m", # Bold yellow
1637
- content_color: "\e[37m" # White
1638
- })
1639
-
1640
- return false if new_name.nil? || new_name.empty?
1641
-
1642
- # Bookmarkを使用してリネーム
1643
- result = @bookmark_manager.instance_variable_get(:@bookmark).rename(old_name, new_name)
1644
-
1645
- @terminal_ui&.refresh_display if @terminal_ui
1646
- result
1647
- end
1648
-
1649
- # プロジェクトモードでブックマークを削除
1650
- def delete_bookmark_in_project_mode
1651
- bookmarks = @bookmark_manager.list
1652
- return false if bookmarks.empty? || @current_index >= bookmarks.length
1653
-
1654
- current_bookmark = bookmarks[@current_index]
1655
- bookmark_name = current_bookmark[:name]
1656
-
1657
- # 確認ダイアログを表示
1658
- content_lines = [
1659
- '',
1660
- "Delete this bookmark?",
1661
- " #{bookmark_name}",
1662
- '',
1663
- ' [Y]es - Delete',
1664
- ' [N]o - Cancel',
1665
- ''
1666
- ]
1667
-
1668
- title = 'Confirm Delete'
1669
- width = [50, bookmark_name.length + 10].max
1670
- height = content_lines.length + 4
1671
- x, y = @dialog_renderer.calculate_center(width, height)
1672
-
1673
- @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1674
- border_color: "\e[31m", # Red (warning)
1675
- title_color: "\e[1;31m", # Bold red
1676
- content_color: "\e[37m" # White
1677
- })
1678
-
1679
- # 確認を待つ
1680
- confirmed = false
1681
- loop do
1682
- input = STDIN.getch.downcase
1683
-
1684
- case input
1685
- when 'y'
1686
- confirmed = true
1687
- break
1688
- when 'n', "\e" # n or ESC
1689
- confirmed = false
1690
- break
1691
- end
1692
- end
1693
-
1694
- @dialog_renderer.clear_area(x, y, width, height)
1695
- @terminal_ui&.refresh_display
1696
-
1697
- return false unless confirmed
1698
-
1699
- # Bookmarkを使用して削除
1700
- result = @bookmark_manager.instance_variable_get(:@bookmark).remove(bookmark_name)
1718
+ # ジョブモード中のキー処理
1719
+ def handle_job_mode_key(key)
1720
+ result = @job_mode.handle_key(key)
1701
1721
 
1702
- # 削除後、選択されていたブックマークがなくなった場合は選択をクリア
1703
- if result && @project_mode.selected_path == current_bookmark[:path]
1704
- @project_mode.clear_selection
1705
- end
1706
-
1707
- # カーソル位置を調整
1708
- if result
1709
- bookmarks_after = @bookmark_manager.list
1710
- @current_index = [@current_index, bookmarks_after.length - 1].min if @current_index >= bookmarks_after.length
1711
- @current_index = 0 if @current_index < 0
1722
+ case result
1723
+ when :exit
1724
+ exit_job_mode
1725
+ true
1726
+ when :show_log
1727
+ # ログ表示は将来実装
1728
+ @terminal_ui&.trigger_job_mode_redraw if @terminal_ui
1729
+ true
1730
+ when true, false
1731
+ @terminal_ui&.trigger_job_mode_redraw if @terminal_ui
1732
+ result
1733
+ else
1734
+ result
1712
1735
  end
1713
-
1714
- @terminal_ui&.refresh_display if @terminal_ui
1715
- result
1716
1736
  end
1717
1737
 
1718
- # プロジェクトモードでブックマークの選択をトグル
1719
- def select_bookmark_in_project_mode
1720
- bookmarks = @bookmark_manager.list
1721
- return false if bookmarks.empty? || @current_index >= bookmarks.length
1722
-
1723
- # ブックマークをプロジェクトとして選択
1724
- @project_mode.select_bookmark(@current_index + 1)
1725
- true
1738
+ # ジョブモード中かどうか
1739
+ def in_job_mode?
1740
+ @job_mode.active?
1726
1741
  end
1727
1742
 
1728
- # プロジェクトモード中かどうか
1729
- def in_project_mode?
1730
- @in_project_mode
1743
+ # ジョブがあるかどうか
1744
+ def has_jobs?
1745
+ @job_manager.has_jobs?
1731
1746
  end
1732
1747
 
1733
- # ログモード中かどうか
1734
- def in_log_mode?
1735
- @in_log_mode
1736
- end
1748
+ # ジョブのステータスバーテキストを取得
1749
+ def job_status_bar_text
1750
+ return nil unless @job_manager.has_jobs?
1737
1751
 
1738
- # ヘルプダイアログを表示
1739
- def show_help_dialog
1740
- @terminal_ui&.show_help_dialog if @terminal_ui
1741
- true
1752
+ @job_manager.status_bar_text
1742
1753
  end
1743
1754
  end
1744
1755
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # Noice風の通知を管理するクラス
5
+ # 画面右上に最大3個までの通知を表示
6
+ class NotificationManager
7
+ MAX_NOTIFICATIONS = 3
8
+ DEFAULT_DISPLAY_DURATION = 3 # 秒
9
+
10
+ attr_reader :notifications
11
+
12
+ def initialize
13
+ @notifications = []
14
+ end
15
+
16
+ # 通知を追加
17
+ # @param name [String] タスク名
18
+ # @param type [Symbol] :success または :error
19
+ # @param duration [Float] タスクの実行時間
20
+ # @param exit_code [Integer, nil] 終了コード(エラー時のみ)
21
+ # @param display_duration [Integer] 通知の表示時間(秒)
22
+ def add(name, type, duration:, exit_code: nil, display_duration: DEFAULT_DISPLAY_DURATION)
23
+ notification = {
24
+ name: name,
25
+ type: type,
26
+ duration: duration,
27
+ exit_code: exit_code,
28
+ created_at: Time.now,
29
+ display_duration: display_duration,
30
+ border_color: type == :success ? :green : :red,
31
+ status_text: build_status_text(type, duration)
32
+ }
33
+
34
+ @notifications << notification
35
+
36
+ # 最大3個を超えた場合、最も古い通知を削除
37
+ @notifications.shift if @notifications.size > MAX_NOTIFICATIONS
38
+ end
39
+
40
+ # 期限切れの通知を削除
41
+ def expire_old_notifications
42
+ now = Time.now
43
+ @notifications.reject! do |notification|
44
+ now - notification[:created_at] > notification[:display_duration]
45
+ end
46
+ end
47
+
48
+ # 通知の数
49
+ # @return [Integer]
50
+ def count
51
+ @notifications.size
52
+ end
53
+
54
+ # 全ての通知をクリア
55
+ def clear
56
+ @notifications.clear
57
+ end
58
+
59
+ private
60
+
61
+ # ステータステキストを生成
62
+ # @param type [Symbol] :success または :error
63
+ # @param duration [Float] 実行時間
64
+ # @return [String]
65
+ def build_status_text(type, duration)
66
+ formatted_duration = format('%.1fs', duration)
67
+ case type
68
+ when :success
69
+ "Done (#{formatted_duration})"
70
+ when :error
71
+ "Failed (#{formatted_duration})"
72
+ else
73
+ formatted_duration
74
+ end
75
+ end
76
+ end
77
+ end