rufio 0.10.0 → 0.20.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.
@@ -48,6 +48,13 @@ module Rufio
48
48
  @command_mode = CommandMode.new
49
49
  @dialog_renderer = DialogRenderer.new
50
50
  @command_mode_ui = CommandModeUI.new(@command_mode, @dialog_renderer)
51
+
52
+ # Project mode
53
+ @project_mode = nil
54
+ @project_command = nil
55
+ @project_log = nil
56
+ @in_project_mode = false
57
+ @in_log_mode = false
51
58
  end
52
59
 
53
60
  def start(directory_listing, keybind_handler, file_preview)
@@ -60,6 +67,9 @@ module Rufio
60
67
  @running = true
61
68
  setup_terminal
62
69
 
70
+ # Show info notices if any
71
+ show_info_notices
72
+
63
73
  begin
64
74
  main_loop
65
75
  ensure
@@ -109,6 +119,12 @@ module Rufio
109
119
  # move cursor to top of screen (don't clear)
110
120
  print "\e[H"
111
121
 
122
+ # プロジェクトモードの場合は専用の画面を描画
123
+ if @in_project_mode
124
+ draw_project_mode_screen
125
+ return
126
+ end
127
+
112
128
  # header (2 lines)
113
129
  draw_header
114
130
  draw_base_directory_info
@@ -418,7 +434,7 @@ module Rufio
418
434
  width = 0
419
435
  string.each_char do |char|
420
436
  # 全角文字の判定
421
- width += if char.ord > 127 || char.match?(/[あ-ん ア-ン 一-龯]/)
437
+ width += if char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/)
422
438
  2
423
439
  else
424
440
  1
@@ -435,7 +451,7 @@ module Rufio
435
451
  result = ''
436
452
 
437
453
  string.each_char do |char|
438
- char_width = char.ord > 127 || char.match?(/[あ-ん ア-ン 一-龯]/) ? 2 : 1
454
+ char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
439
455
 
440
456
  if current_width + char_width > max_width
441
457
  # "..."を追加できるかチェック
@@ -461,7 +477,7 @@ module Rufio
461
477
  punct_break_point = nil
462
478
 
463
479
  line.each_char.with_index do |char, index|
464
- char_width = char.ord > 127 || char.match?(/[あ-ん ア-ン 一-龯]/) ? 2 : 1
480
+ char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
465
481
 
466
482
  break if current_width + char_width > max_width
467
483
 
@@ -563,7 +579,7 @@ module Rufio
563
579
  end
564
580
 
565
581
  # キーバインドハンドラーに処理を委譲
566
- result = @keybind_handler.handle_key(input)
582
+ _result = @keybind_handler.handle_key(input)
567
583
 
568
584
  # 終了処理(qキーのみ)
569
585
  if input == 'q'
@@ -625,6 +641,538 @@ module Rufio
625
641
  # 画面を再描画
626
642
  draw_screen
627
643
  end
644
+
645
+ # Show info notices from the info directory if any are unread
646
+ def show_info_notices
647
+ require_relative 'info_notice'
648
+ info_notice = InfoNotice.new
649
+ notices = info_notice.unread_notices
650
+
651
+ notices.each do |notice|
652
+ show_info_notice(notice, info_notice)
653
+ end
654
+ end
655
+
656
+ # Show a single info notice
657
+ # @param notice [Hash] Notice hash with :title and :content
658
+ # @param info_notice [InfoNotice] InfoNotice instance to mark as shown
659
+ def show_info_notice(notice, info_notice)
660
+ # Calculate window dimensions
661
+ width = [@screen_width - 10, 70].min
662
+ # Calculate height based on content length
663
+ content_length = notice[:content].length
664
+ height = [content_length + 4, @screen_height - 4].min # +4 for borders and title
665
+ x = (@screen_width - width) / 2
666
+ y = (@screen_height - height) / 2
667
+
668
+ # Display the notice window
669
+ @dialog_renderer.draw_floating_window(
670
+ x, y, width, height,
671
+ notice[:title],
672
+ notice[:content],
673
+ {
674
+ border_color: "\e[36m", # Cyan
675
+ title_color: "\e[1;36m", # Bold cyan
676
+ content_color: "\e[37m" # White
677
+ }
678
+ )
679
+
680
+ # Force flush to ensure display
681
+ $stdout.flush
682
+
683
+ # Wait for any key press
684
+ require 'io/console'
685
+ IO.console.getch
686
+
687
+ # Mark as shown
688
+ info_notice.mark_as_shown(notice[:file])
689
+
690
+ # Clear the notice window
691
+ @dialog_renderer.clear_area(x, y, width, height)
692
+
693
+ # Redraw the screen
694
+ draw_screen
695
+ end
696
+
697
+ # プロジェクトモードを設定
698
+ def set_project_mode(project_mode, project_command, project_log)
699
+ @project_mode = project_mode
700
+ @project_command = project_command
701
+ @project_log = project_log
702
+ @in_project_mode = true
703
+ @in_log_mode = false
704
+ refresh_display
705
+ draw_screen
706
+ end
707
+
708
+ # プロジェクトモードを終了
709
+ def exit_project_mode
710
+ @in_project_mode = false
711
+ @in_log_mode = false
712
+ @project_mode = nil
713
+ @project_command = nil
714
+ @project_log = nil
715
+ refresh_display
716
+ draw_screen
717
+ end
718
+
719
+ # ログモードに入る
720
+ def enter_log_mode(project_log)
721
+ @in_log_mode = true
722
+ @project_log = project_log
723
+ refresh_display
724
+ draw_screen
725
+ end
726
+
727
+ # プロジェクトモード画面を描画
728
+ def draw_project_mode_screen
729
+ # header
730
+ print "\e[1;1H" # Move to top-left
731
+ header = @in_log_mode ? "📋 Project Mode - Logs" : "📁 Project Mode - Bookmarks"
732
+ print "\e[44m\e[97m#{header.ljust(@screen_width)}\e[0m\n"
733
+ print "\e[0m#{' ' * @screen_width}\n"
734
+
735
+ # calculate dimensions
736
+ content_height = @screen_height - HEADER_FOOTER_MARGIN
737
+ left_width = (@screen_width * LEFT_PANEL_RATIO).to_i
738
+ right_width = @screen_width - left_width
739
+
740
+ if @in_log_mode
741
+ # ログモード: ログファイル一覧と内容
742
+ draw_log_list(left_width, content_height)
743
+ draw_log_preview(right_width, content_height, left_width)
744
+ else
745
+ # ブックマークモード: プロジェクト一覧と詳細
746
+ draw_bookmark_list(left_width, content_height)
747
+ draw_bookmark_detail(right_width, content_height, left_width)
748
+ end
749
+
750
+ # footer(通常モードと同じスタイル)
751
+ footer_line = @screen_height
752
+ print "\e[#{footer_line};1H"
753
+ footer_text = if @in_log_mode
754
+ "ESC:exit log j/k:move"
755
+ else
756
+ "SPACE:select l:logs ::cmd r:rename d:delete ESC:exit j/k:move"
757
+ end
758
+ # 文字列を確実に画面幅に合わせる
759
+ footer_content = footer_text.ljust(@screen_width)[0...@screen_width]
760
+ print "\e[7m#{footer_content}\e[0m"
761
+
762
+ # move cursor to invisible position
763
+ print "\e[#{@screen_height};#{@screen_width}H"
764
+ end
765
+
766
+ # ブックマーク一覧を描画
767
+ def draw_bookmark_list(width, height)
768
+ bookmarks = @project_mode.list_bookmarks
769
+ current_index = @keybind_handler.current_index
770
+
771
+ print "\e[#{CONTENT_START_LINE};1H"
772
+
773
+ if bookmarks.empty?
774
+ print " No bookmarks found"
775
+ (height - 1).times { puts ' ' * width }
776
+ return
777
+ end
778
+
779
+ selected_name = @project_mode.selected_name
780
+
781
+ bookmarks.each_with_index do |bookmark, index|
782
+ line_num = CONTENT_START_LINE + index
783
+ break if index >= height
784
+
785
+ # 選択マーク(通常モードと同じ)
786
+ is_project_selected = (bookmark[:name] == selected_name)
787
+ selection_mark = is_project_selected ? "✓ " : " "
788
+
789
+ # ブックマーク名を表示
790
+ name = bookmark[:name]
791
+ max_name_length = width - 4 # selection_mark分を除く
792
+ display_name = name.length > max_name_length ? name[0...max_name_length - 3] + '...' : name
793
+ line_content = "#{selection_mark}#{display_name}".ljust(width)
794
+
795
+ if index == current_index
796
+ # カーソル位置は選択色でハイライト
797
+ selected_color = ColorHelper.color_to_selected_ansi(ConfigLoader.colors[:selected])
798
+ print "\e[#{line_num};1H#{selected_color}#{line_content[0...width]}#{ColorHelper.reset}"
799
+ else
800
+ # 選択済みブックマークは緑背景、黒文字
801
+ if is_project_selected
802
+ print "\e[#{line_num};1H\e[42m\e[30m#{line_content[0...width]}\e[0m"
803
+ else
804
+ print "\e[#{line_num};1H#{line_content[0...width]}"
805
+ end
806
+ end
807
+ end
808
+
809
+ # 残りの行をクリア
810
+ remaining_lines = height - bookmarks.length
811
+ remaining_lines.times do |i|
812
+ line_num = CONTENT_START_LINE + bookmarks.length + i
813
+ print "\e[#{line_num};1H#{' ' * width}"
814
+ end
815
+ end
816
+
817
+ # ブックマーク詳細を描画
818
+ def draw_bookmark_detail(width, height, left_offset)
819
+ bookmarks = @project_mode.list_bookmarks
820
+ current_index = @keybind_handler.current_index
821
+
822
+ return if bookmarks.empty? || current_index >= bookmarks.length
823
+
824
+ bookmark = bookmarks[current_index]
825
+ path = bookmark[:path]
826
+
827
+ # ディレクトリ内容を取得
828
+ details = [
829
+ "Project: #{bookmark[:name]}",
830
+ "Path: #{path}",
831
+ "",
832
+ "Directory contents:",
833
+ ""
834
+ ]
835
+
836
+ # ディレクトリが存在する場合、内容を表示
837
+ if Dir.exist?(path)
838
+ begin
839
+ entries = Dir.entries(path).reject { |e| e == '.' || e == '..' }.sort
840
+
841
+ # 最大表示数を計算(ヘッダー分を引く)
842
+ max_entries = height - details.length
843
+
844
+ entries.take(max_entries).each do |entry|
845
+ full_path = File.join(path, entry)
846
+ icon = File.directory?(full_path) ? '📁' : '📄'
847
+ details << " #{icon} #{entry}"
848
+ end
849
+
850
+ # 表示しきれない場合
851
+ if entries.length > max_entries
852
+ details << " ... and #{entries.length - max_entries} more"
853
+ end
854
+ rescue => e
855
+ details << " Error reading directory: #{e.message}"
856
+ end
857
+ else
858
+ details << " Directory does not exist"
859
+ end
860
+
861
+ # 各行にセパレータと内容を表示(通常モードと同じ)
862
+ height.times do |i|
863
+ line_num = CONTENT_START_LINE + i
864
+
865
+ # セパレータを表示
866
+ cursor_position = left_offset + CURSOR_OFFSET
867
+ print "\e[#{line_num};#{cursor_position}H"
868
+ print '│'
869
+
870
+ # 右画面の内容を表示
871
+ if i < details.length
872
+ line = details[i]
873
+ safe_width = width - 2
874
+ content = " #{line}"
875
+ content = content[0...safe_width] if content.length > safe_width
876
+ print content
877
+
878
+ # 残りをスペースで埋める
879
+ remaining = safe_width - content.length
880
+ print ' ' * remaining if remaining > 0
881
+ else
882
+ # 空行
883
+ print ' ' * (width - 2)
884
+ end
885
+ end
886
+ end
887
+
888
+ # ログファイル一覧を描画
889
+ def draw_log_list(width, height)
890
+ log_files = @project_log.list_log_files
891
+ current_index = @keybind_handler.current_index
892
+
893
+ print "\e[#{CONTENT_START_LINE};1H"
894
+
895
+ if log_files.empty?
896
+ print " No log files found"
897
+ (height - 1).times { puts ' ' * width }
898
+ return
899
+ end
900
+
901
+ log_files.each_with_index do |filename, index|
902
+ line_num = CONTENT_START_LINE + index
903
+ break if index >= height
904
+
905
+ cursor_mark = index == current_index ? '>' : ' '
906
+ display_name = filename.ljust(width - 3)
907
+
908
+ if index == current_index
909
+ print "\e[#{line_num};1H\e[7m#{cursor_mark} #{display_name[0...width-3]}\e[0m"
910
+ else
911
+ print "\e[#{line_num};1H #{display_name[0...width-3]}"
912
+ end
913
+ end
914
+
915
+ # 残りの行をクリア
916
+ remaining_lines = height - log_files.length
917
+ remaining_lines.times do |i|
918
+ line_num = CONTENT_START_LINE + log_files.length + i
919
+ print "\e[#{line_num};1H#{' ' * width}"
920
+ end
921
+ end
922
+
923
+ # ログプレビューを描画
924
+ def draw_log_preview(width, height, left_offset)
925
+ log_files = @project_log.list_log_files
926
+ current_index = @keybind_handler.current_index
927
+
928
+ return if log_files.empty? || current_index >= log_files.length
929
+
930
+ filename = log_files[current_index]
931
+ content = @project_log.preview(filename)
932
+
933
+ lines = content.split("\n")
934
+
935
+ # 各行にセパレータと内容を表示(通常モードと同じ)
936
+ height.times do |i|
937
+ line_num = CONTENT_START_LINE + i
938
+
939
+ # セパレータを表示
940
+ cursor_position = left_offset + CURSOR_OFFSET
941
+ print "\e[#{line_num};#{cursor_position}H"
942
+ print '│'
943
+
944
+ # 右画面の内容を表示
945
+ if i < lines.length
946
+ line = lines[i]
947
+ safe_width = width - 2
948
+ content = " #{line}"
949
+ content = content[0...safe_width] if content.length > safe_width
950
+ print content
951
+
952
+ # 残りをスペースで埋める
953
+ remaining = safe_width - content.length
954
+ print ' ' * remaining if remaining > 0
955
+ else
956
+ # 空行
957
+ print ' ' * (width - 2)
958
+ end
959
+ end
960
+ end
961
+
962
+ # ログモードを終了してプロジェクトモードに戻る
963
+ def exit_log_mode
964
+ @in_log_mode = false
965
+ refresh_display
966
+ draw_screen
967
+ end
968
+
969
+ # プロジェクト未選択メッセージ
970
+ def show_project_not_selected_message
971
+ content_lines = [
972
+ '',
973
+ 'Please select a project first by pressing SPACE',
974
+ '',
975
+ 'Press any key to continue...'
976
+ ]
977
+
978
+ width = 50
979
+ height = 8
980
+ x, y = @dialog_renderer.calculate_center(width, height)
981
+
982
+ @dialog_renderer.draw_floating_window(x, y, width, height, 'No Project Selected', content_lines, {
983
+ border_color: "\e[33m", # Yellow (warning)
984
+ title_color: "\e[1;33m", # Bold yellow
985
+ content_color: "\e[37m" # White
986
+ })
987
+
988
+ require 'io/console'
989
+ IO.console.getch
990
+ @dialog_renderer.clear_area(x, y, width, height)
991
+
992
+ # 画面を再描画
993
+ refresh_display
994
+ draw_screen
995
+ end
996
+
997
+ # プロジェクトモードでコマンドを実行
998
+ def activate_project_command_mode(project_mode, project_command, project_log)
999
+ return unless project_mode.selected_path
1000
+
1001
+ # スクリプトまたはコマンドを選択
1002
+ choice = show_script_or_command_dialog(project_mode.selected_name, project_command)
1003
+ return unless choice
1004
+
1005
+ command = nil
1006
+ result = nil
1007
+
1008
+ if choice[:type] == :script
1009
+ # スクリプトを実行
1010
+ command = "ruby script: #{choice[:value]}"
1011
+ result = project_command.execute_script(choice[:value], project_mode.selected_path)
1012
+ else
1013
+ # 通常のコマンドを実行
1014
+ command = choice[:value]
1015
+ result = project_command.execute(command, project_mode.selected_path)
1016
+ end
1017
+
1018
+ # ログを保存
1019
+ project_log.save(project_mode.selected_name, command, result[:output])
1020
+
1021
+ # 結果を表示
1022
+ show_project_command_result_dialog(command, result)
1023
+
1024
+ # 画面を再描画
1025
+ refresh_display
1026
+ draw_screen
1027
+ end
1028
+
1029
+ # スクリプトまたはコマンドを選択
1030
+ def show_script_or_command_dialog(project_name, project_command)
1031
+ scripts = project_command.list_scripts
1032
+
1033
+ content_lines = [
1034
+ '',
1035
+ "Project: #{project_name}",
1036
+ ''
1037
+ ]
1038
+
1039
+ if scripts.empty?
1040
+ content_lines << 'No scripts found in scripts directory'
1041
+ content_lines << " (#{project_command.scripts_dir})"
1042
+ content_lines << ''
1043
+ content_lines << 'Press C to enter custom command'
1044
+ content_lines << 'Press ESC to cancel'
1045
+ else
1046
+ content_lines << 'Available scripts:'
1047
+ content_lines << ''
1048
+ scripts.each_with_index do |script, index|
1049
+ content_lines << " #{index + 1}. #{script}"
1050
+ end
1051
+ content_lines << ''
1052
+ content_lines << 'Press 1-9 to select script'
1053
+ content_lines << 'Press C to enter custom command'
1054
+ content_lines << 'Press ESC to cancel'
1055
+ end
1056
+
1057
+ width = 70
1058
+ height = [content_lines.length + 4, 25].min
1059
+ x, y = @dialog_renderer.calculate_center(width, height)
1060
+
1061
+ @dialog_renderer.draw_floating_window(x, y, width, height, 'Execute in Project', content_lines, {
1062
+ border_color: "\e[32m",
1063
+ title_color: "\e[1;32m",
1064
+ content_color: "\e[37m"
1065
+ })
1066
+
1067
+ require 'io/console'
1068
+ choice = nil
1069
+
1070
+ loop do
1071
+ input = IO.console.getch.downcase
1072
+
1073
+ case input
1074
+ when "\e" # ESC
1075
+ break
1076
+ when 'c' # Custom command
1077
+ @dialog_renderer.clear_area(x, y, width, height)
1078
+ command = show_project_command_input_dialog(project_name)
1079
+ choice = { type: :command, value: command } if command && !command.empty?
1080
+ break
1081
+ when '1'..'9'
1082
+ number = input.to_i
1083
+ if number > 0 && number <= scripts.length
1084
+ choice = { type: :script, value: scripts[number - 1] }
1085
+ break
1086
+ end
1087
+ end
1088
+ end
1089
+
1090
+ @dialog_renderer.clear_area(x, y, width, height)
1091
+ choice
1092
+ end
1093
+
1094
+ # プロジェクトコマンド入力ダイアログ
1095
+ def show_project_command_input_dialog(project_name)
1096
+ title = "Execute Command in: #{project_name}"
1097
+ prompt = "Enter command:"
1098
+
1099
+ @dialog_renderer.show_input_dialog(title, prompt, {
1100
+ border_color: "\e[32m", # Green
1101
+ title_color: "\e[1;32m", # Bold green
1102
+ content_color: "\e[37m" # White
1103
+ })
1104
+ end
1105
+
1106
+ # プロジェクトコマンド結果ダイアログ
1107
+ def show_project_command_result_dialog(command, result)
1108
+ title = result[:success] ? "Command Success" : "Command Failed"
1109
+
1110
+ # 出力を最初の10行まで表示
1111
+ output_lines = (result[:output] || result[:error] || '').split("\n").take(10)
1112
+
1113
+ content_lines = [
1114
+ '',
1115
+ "Command: #{command}",
1116
+ '',
1117
+ "Output:",
1118
+ ''
1119
+ ] + output_lines
1120
+
1121
+ if output_lines.length >= 10
1122
+ content_lines << '... (see log for full output)'
1123
+ end
1124
+
1125
+ content_lines << ''
1126
+ content_lines << 'Press any key to continue...'
1127
+
1128
+ width = 80
1129
+ height = [content_lines.length + 4, 20].min
1130
+ x, y = @dialog_renderer.calculate_center(width, height)
1131
+
1132
+ border_color = result[:success] ? "\e[32m" : "\e[31m" # Green or Red
1133
+ title_color = result[:success] ? "\e[1;32m" : "\e[1;31m"
1134
+
1135
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1136
+ border_color: border_color,
1137
+ title_color: title_color,
1138
+ content_color: "\e[37m"
1139
+ })
1140
+
1141
+ require 'io/console'
1142
+ IO.console.getch
1143
+ @dialog_renderer.clear_area(x, y, width, height)
1144
+ end
1145
+
1146
+ # プロジェクト選択時の表示
1147
+ def show_project_selected
1148
+ # 選択完了メッセージを表示
1149
+ content_lines = [
1150
+ '',
1151
+ 'Project selected!',
1152
+ '',
1153
+ 'You can now press : to execute commands',
1154
+ '',
1155
+ 'Press any key to continue...'
1156
+ ]
1157
+
1158
+ width = 50
1159
+ height = 10
1160
+ x, y = @dialog_renderer.calculate_center(width, height)
1161
+
1162
+ @dialog_renderer.draw_floating_window(x, y, width, height, 'Project Selected', content_lines, {
1163
+ border_color: "\e[32m", # Green
1164
+ title_color: "\e[1;32m", # Bold green
1165
+ content_color: "\e[37m" # White
1166
+ })
1167
+
1168
+ require 'io/console'
1169
+ IO.console.getch
1170
+ @dialog_renderer.clear_area(x, y, width, height)
1171
+
1172
+ # 画面を再描画
1173
+ refresh_display
1174
+ draw_screen
1175
+ end
628
1176
  end
629
1177
  end
630
1178
 
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.10.0'
4
+ VERSION = '0.20.0'
5
5
  end
data/lib/rufio.rb CHANGED
@@ -28,6 +28,11 @@ require_relative "rufio/plugin_manager"
28
28
  require_relative "rufio/command_mode"
29
29
  require_relative "rufio/command_mode_ui"
30
30
 
31
+ # プロジェクトモード
32
+ require_relative "rufio/project_mode"
33
+ require_relative "rufio/project_command"
34
+ require_relative "rufio/project_log"
35
+
31
36
  module Rufio
32
37
  class Error < StandardError; end
33
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rufio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
@@ -118,6 +118,7 @@ extra_rdoc_files: []
118
118
  files:
119
119
  - CHANGELOG.md
120
120
  - CHANGELOG_v0.10.0.md
121
+ - CHANGELOG_v0.20.0.md
121
122
  - CHANGELOG_v0.4.0.md
122
123
  - CHANGELOG_v0.5.0.md
123
124
  - CHANGELOG_v0.6.0.md
@@ -131,6 +132,7 @@ files:
131
132
  - config_example.rb
132
133
  - docs/PLUGIN_GUIDE.md
133
134
  - docs/plugin_example.rb
135
+ - info/welcome.txt
134
136
  - lib/rufio.rb
135
137
  - lib/rufio/application.rb
136
138
  - lib/rufio/bookmark.rb
@@ -147,12 +149,16 @@ files:
147
149
  - lib/rufio/file_preview.rb
148
150
  - lib/rufio/filter_manager.rb
149
151
  - lib/rufio/health_checker.rb
152
+ - lib/rufio/info_notice.rb
150
153
  - lib/rufio/keybind_handler.rb
151
154
  - lib/rufio/logger.rb
152
155
  - lib/rufio/plugin.rb
153
156
  - lib/rufio/plugin_config.rb
154
157
  - lib/rufio/plugin_manager.rb
155
158
  - lib/rufio/plugins/file_operations.rb
159
+ - lib/rufio/project_command.rb
160
+ - lib/rufio/project_log.rb
161
+ - lib/rufio/project_mode.rb
156
162
  - lib/rufio/selection_manager.rb
157
163
  - lib/rufio/terminal_ui.rb
158
164
  - lib/rufio/text_utils.rb