rufio 0.11.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.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'time'
5
+
6
+ module Rufio
7
+ # プロジェクトログ - コマンド実行ログを管理する
8
+ class ProjectLog
9
+ def initialize(log_dir)
10
+ @log_dir = log_dir
11
+ FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
12
+ end
13
+
14
+ # ログを保存する
15
+ # @param project_name [String] プロジェクト名
16
+ # @param command [String] 実行したコマンド
17
+ # @param output [String] コマンドの出力
18
+ # @return [String] 保存したログファイルのパス
19
+ def save(project_name, command, output)
20
+ timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
21
+ log_filename = "#{project_name}_#{timestamp}.log"
22
+ log_path = File.join(@log_dir, log_filename)
23
+
24
+ log_content = <<~LOG
25
+ Project: #{project_name}
26
+ Command: #{command}
27
+ Timestamp: #{Time.now}
28
+
29
+ Output:
30
+ #{output}
31
+ LOG
32
+
33
+ File.write(log_path, log_content)
34
+ log_path
35
+ end
36
+
37
+ # ログディレクトリに移動する
38
+ # @return [Hash] ログディレクトリ情報
39
+ def navigate_to_log_dir
40
+ {
41
+ path: @log_dir
42
+ }
43
+ end
44
+
45
+ # ログファイルの一覧を取得(新しい順)
46
+ # @return [Array<String>] ログファイル名の配列
47
+ def list_log_files
48
+ log_files = Dir.glob(File.join(@log_dir, '*.log'))
49
+
50
+ # ファイルの更新時刻でソート(新しい順)
51
+ log_files.sort_by { |f| -File.mtime(f).to_i }
52
+ .map { |f| File.basename(f) }
53
+ end
54
+
55
+ # ログファイルのプレビューを取得
56
+ # @param filename [String] ログファイル名
57
+ # @return [String] ログファイルの内容
58
+ def preview(filename)
59
+ log_path = File.join(@log_dir, filename)
60
+
61
+ return '' unless File.exist?(log_path)
62
+
63
+ File.read(log_path)
64
+ rescue StandardError
65
+ ''
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rufio
4
+ # プロジェクトモード - ブックマークしたプロジェクトの管理とコマンド実行
5
+ class ProjectMode
6
+ attr_reader :selected_path, :selected_name
7
+
8
+ def initialize(bookmark, log_dir)
9
+ @bookmark = bookmark
10
+ @log_dir = log_dir
11
+ @active = false
12
+ @selected_path = nil
13
+ @selected_name = nil
14
+ end
15
+
16
+ # プロジェクトモードをアクティブにする
17
+ def activate
18
+ @active = true
19
+ end
20
+
21
+ # プロジェクトモードを非アクティブにする
22
+ def deactivate
23
+ @active = false
24
+ @selected_path = nil
25
+ @selected_name = nil
26
+ end
27
+
28
+ # プロジェクトモードがアクティブかどうか
29
+ def active?
30
+ @active
31
+ end
32
+
33
+ # ブックマーク一覧を取得
34
+ def list_bookmarks
35
+ return [] unless @active
36
+
37
+ @bookmark.list
38
+ end
39
+
40
+ # ブックマークを番号で選択
41
+ def select_bookmark(number)
42
+ return false unless @active
43
+
44
+ bookmark = @bookmark.find_by_number(number)
45
+ return false unless bookmark
46
+
47
+ @selected_path = bookmark[:path]
48
+ @selected_name = bookmark[:name]
49
+ true
50
+ end
51
+
52
+ # 選択をクリア
53
+ def clear_selection
54
+ @selected_path = nil
55
+ @selected_name = nil
56
+ end
57
+ end
58
+ end
@@ -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)
@@ -112,6 +119,12 @@ module Rufio
112
119
  # move cursor to top of screen (don't clear)
113
120
  print "\e[H"
114
121
 
122
+ # プロジェクトモードの場合は専用の画面を描画
123
+ if @in_project_mode
124
+ draw_project_mode_screen
125
+ return
126
+ end
127
+
115
128
  # header (2 lines)
116
129
  draw_header
117
130
  draw_base_directory_info
@@ -421,7 +434,7 @@ module Rufio
421
434
  width = 0
422
435
  string.each_char do |char|
423
436
  # 全角文字の判定
424
- width += if char.ord > 127 || char.match?(/[あ-ん ア-ン 一-龯]/)
437
+ width += if char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/)
425
438
  2
426
439
  else
427
440
  1
@@ -438,7 +451,7 @@ module Rufio
438
451
  result = ''
439
452
 
440
453
  string.each_char do |char|
441
- char_width = char.ord > 127 || char.match?(/[あ-ん ア-ン 一-龯]/) ? 2 : 1
454
+ char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
442
455
 
443
456
  if current_width + char_width > max_width
444
457
  # "..."を追加できるかチェック
@@ -464,7 +477,7 @@ module Rufio
464
477
  punct_break_point = nil
465
478
 
466
479
  line.each_char.with_index do |char, index|
467
- char_width = char.ord > 127 || char.match?(/[あ-ん ア-ン 一-龯]/) ? 2 : 1
480
+ char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
468
481
 
469
482
  break if current_width + char_width > max_width
470
483
 
@@ -566,7 +579,7 @@ module Rufio
566
579
  end
567
580
 
568
581
  # キーバインドハンドラーに処理を委譲
569
- result = @keybind_handler.handle_key(input)
582
+ _result = @keybind_handler.handle_key(input)
570
583
 
571
584
  # 終了処理(qキーのみ)
572
585
  if input == 'q'
@@ -680,6 +693,486 @@ module Rufio
680
693
  # Redraw the screen
681
694
  draw_screen
682
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
683
1176
  end
684
1177
  end
685
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.11.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