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.
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Rufio
6
+ # Manages info notices from the info directory
7
+ class InfoNotice
8
+ INFO_DIR = File.join(File.dirname(__FILE__), '..', '..', 'info')
9
+ NOTICE_TRACKING_DIR = File.join(Dir.home, '.config', 'rufio', 'notices')
10
+
11
+ attr_accessor :info_dir, :tracking_dir
12
+
13
+ def initialize(info_dir: nil, tracking_dir: nil)
14
+ @info_dir = info_dir || INFO_DIR
15
+ @tracking_dir = tracking_dir || NOTICE_TRACKING_DIR
16
+ ensure_tracking_directory
17
+ end
18
+
19
+ # Get all available notices that haven't been shown
20
+ # @return [Array<Hash>] Array of notice hashes with :file, :title, :content
21
+ def unread_notices
22
+ return [] unless Dir.exist?(@info_dir)
23
+
24
+ Dir.glob(File.join(@info_dir, '*.txt')).map do |file_path|
25
+ next if shown?(file_path)
26
+
27
+ {
28
+ file: file_path,
29
+ filename: File.basename(file_path),
30
+ title: extract_title(file_path),
31
+ content: read_content(file_path)
32
+ }
33
+ end.compact
34
+ end
35
+
36
+ # Check if a notice has been shown
37
+ # @param file_path [String] Path to the notice file
38
+ # @return [Boolean] true if already shown
39
+ def shown?(file_path)
40
+ tracking_file = tracking_file_path(file_path)
41
+ File.exist?(tracking_file)
42
+ end
43
+
44
+ # Mark a notice as shown
45
+ # @param file_path [String] Path to the notice file
46
+ def mark_as_shown(file_path)
47
+ tracking_file = tracking_file_path(file_path)
48
+ FileUtils.touch(tracking_file)
49
+ end
50
+
51
+ # Extract title from the first line of the file
52
+ # @param file_path [String] Path to the notice file
53
+ # @return [String] The title
54
+ def extract_title(file_path)
55
+ first_line = File.open(file_path, &:readline).strip
56
+ # Remove markdown heading markers if present
57
+ first_line.gsub(/^#+\s*/, '')
58
+ rescue StandardError
59
+ File.basename(file_path, '.txt')
60
+ end
61
+
62
+ # Read the content of a notice file
63
+ # @param file_path [String] Path to the notice file
64
+ # @return [Array<String>] Content lines
65
+ def read_content(file_path)
66
+ lines = File.readlines(file_path, chomp: true)
67
+
68
+ # Skip the first line if it's a markdown heading (title)
69
+ lines = lines.drop(1) if lines.first&.start_with?('#')
70
+
71
+ # Add empty lines at the beginning and end for padding
72
+ [''] + lines + ['', 'Press any key to continue...', '']
73
+ rescue StandardError => e
74
+ [
75
+ '',
76
+ "Error reading notice: #{e.message}",
77
+ '',
78
+ 'Press any key to continue...',
79
+ ''
80
+ ]
81
+ end
82
+
83
+ private
84
+
85
+ def ensure_tracking_directory
86
+ FileUtils.mkdir_p(@tracking_dir) unless Dir.exist?(@tracking_dir)
87
+ end
88
+
89
+ # Get the tracking file path for a given notice file
90
+ # @param file_path [String] Path to the notice file
91
+ # @return [String] Path to the tracking file
92
+ def tracking_file_path(file_path)
93
+ filename = File.basename(file_path)
94
+ # Use MD5 hash of the filename to avoid issues with special characters
95
+ require 'digest'
96
+ hash = Digest::MD5.hexdigest(filename)
97
+ File.join(@tracking_dir, ".#{hash}_shown")
98
+ end
99
+ end
100
+ end
@@ -44,6 +44,14 @@ 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
+
47
55
  # Legacy fields for backward compatibility
48
56
  @base_directory = nil
49
57
  end
@@ -72,6 +80,11 @@ module Rufio
72
80
  def handle_key(key)
73
81
  return false unless @directory_listing
74
82
 
83
+ # プロジェクトモード中の特別処理
84
+ if @in_project_mode
85
+ return handle_project_mode_key(key)
86
+ end
87
+
75
88
  # フィルターモード中は他のキーバインドを無効化
76
89
  return handle_filter_input(key) if @filter_manager.filter_mode
77
90
 
@@ -88,8 +101,12 @@ module Rufio
88
101
  move_to_top
89
102
  when 'G'
90
103
  move_to_bottom
91
- when 'r'
104
+ when 'R' # R - refresh
92
105
  refresh
106
+ when 'r' # r - rename file/directory
107
+ rename_current_file
108
+ when 'd' # d - delete file/directory with confirmation
109
+ delete_current_file_with_confirmation
93
110
  when 'o' # o
94
111
  open_current_file
95
112
  when 'e' # e - open directory in file explorer
@@ -126,12 +143,14 @@ module Rufio
126
143
  create_directory
127
144
  when 'm' # m - move selected files to base directory
128
145
  move_selected_to_base
129
- when 'p' # p - copy selected files to base directory
146
+ when 'C' # C - copy selected files to base directory
130
147
  copy_selected_to_base
131
148
  when 'x' # x - delete selected files
132
149
  delete_selected_files
133
- when 'b' # b - bookmark operations
134
- show_bookmark_menu
150
+ when 'p' # p - project mode
151
+ enter_project_mode
152
+ when 'b' # b - add bookmark
153
+ add_bookmark
135
154
  when 'z' # z - zoxide history navigation
136
155
  show_zoxide_menu
137
156
  when '1', '2', '3', '4', '5', '6', '7', '8', '9' # number keys - go to bookmark
@@ -369,12 +388,15 @@ module Rufio
369
388
  def create_file
370
389
  current_path = @directory_listing&.current_path || Dir.pwd
371
390
 
372
- # カーソルを画面下部に移動して入力プロンプトを表示
373
- move_to_input_line
374
- print ConfigLoader.message('keybind.input_filename')
375
- STDOUT.flush
391
+ # ダイアログレンダラーを使用して入力ダイアログを表示
392
+ title = "Create File"
393
+ prompt = "Enter file name:"
394
+ filename = @dialog_renderer.show_input_dialog(title, prompt, {
395
+ border_color: "\e[32m", # Green
396
+ title_color: "\e[1;32m", # Bold green
397
+ content_color: "\e[37m" # White
398
+ })
376
399
 
377
- filename = read_line_with_escape
378
400
  return false if filename.nil? || filename.empty?
379
401
 
380
402
  # FileOperationsを使用してファイルを作成
@@ -390,22 +412,22 @@ module Rufio
390
412
  @current_index = new_file_index if new_file_index
391
413
  end
392
414
 
393
- # 結果を表示
394
- puts "\n#{result.message}"
395
- print ConfigLoader.message('keybind.press_any_key')
396
- STDIN.getch
415
+ @terminal_ui&.refresh_display
397
416
  result.success
398
417
  end
399
418
 
400
419
  def create_directory
401
420
  current_path = @directory_listing&.current_path || Dir.pwd
402
421
 
403
- # カーソルを画面下部に移動して入力プロンプトを表示
404
- move_to_input_line
405
- print ConfigLoader.message('keybind.input_dirname')
406
- STDOUT.flush
422
+ # ダイアログレンダラーを使用して入力ダイアログを表示
423
+ title = "Create Directory"
424
+ prompt = "Enter directory name:"
425
+ dirname = @dialog_renderer.show_input_dialog(title, prompt, {
426
+ border_color: "\e[34m", # Blue
427
+ title_color: "\e[1;34m", # Bold blue
428
+ content_color: "\e[37m" # White
429
+ })
407
430
 
408
- dirname = read_line_with_escape
409
431
  return false if dirname.nil? || dirname.empty?
410
432
 
411
433
  # FileOperationsを使用してディレクトリを作成
@@ -421,10 +443,110 @@ module Rufio
421
443
  @current_index = new_dir_index if new_dir_index
422
444
  end
423
445
 
424
- # 結果を表示
425
- puts "\n#{result.message}"
426
- print ConfigLoader.message('keybind.press_any_key')
427
- STDIN.getch
446
+ @terminal_ui&.refresh_display
447
+ result.success
448
+ end
449
+
450
+ def rename_current_file
451
+ current_item = current_entry()
452
+ return false unless current_item
453
+
454
+ current_name = current_item[:name]
455
+ current_path = @directory_listing&.current_path || Dir.pwd
456
+
457
+ # ダイアログレンダラーを使用して入力ダイアログを表示
458
+ title = "Rename: #{current_name}"
459
+ prompt = "Enter new name:"
460
+ new_name = @dialog_renderer.show_input_dialog(title, prompt, {
461
+ border_color: "\e[33m", # Yellow
462
+ title_color: "\e[1;33m", # Bold yellow
463
+ content_color: "\e[37m" # White
464
+ })
465
+
466
+ return false if new_name.nil? || new_name.empty?
467
+
468
+ # FileOperationsを使用してリネーム
469
+ result = @file_operations.rename(current_path, current_name, new_name)
470
+
471
+ # ディレクトリ表示を更新
472
+ if result.success
473
+ @directory_listing.refresh
474
+
475
+ # リネームしたファイルを選択状態にする
476
+ entries = @directory_listing.list_entries
477
+ new_index = entries.find_index { |entry| entry[:name] == new_name }
478
+ @current_index = new_index if new_index
479
+ end
480
+
481
+ @terminal_ui&.refresh_display
482
+ result.success
483
+ end
484
+
485
+ def delete_current_file_with_confirmation
486
+ current_item = current_entry()
487
+ return false unless current_item
488
+
489
+ current_name = current_item[:name]
490
+ current_path = @directory_listing&.current_path || Dir.pwd
491
+ is_directory = current_item[:type] == :directory
492
+
493
+ # 確認ダイアログを表示
494
+ type_text = is_directory ? 'directory' : 'file'
495
+ content_lines = [
496
+ '',
497
+ "Delete this #{type_text}?",
498
+ " #{current_name}",
499
+ '',
500
+ ' [Y]es - Delete',
501
+ ' [N]o - Cancel',
502
+ ''
503
+ ]
504
+
505
+ title = 'Confirm Delete'
506
+ width = [50, current_name.length + 10].max
507
+ height = content_lines.length + 4
508
+ x, y = @dialog_renderer.calculate_center(width, height)
509
+
510
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
511
+ border_color: "\e[31m", # Red (warning)
512
+ title_color: "\e[1;31m", # Bold red
513
+ content_color: "\e[37m" # White
514
+ })
515
+
516
+ # 確認を待つ
517
+ confirmed = false
518
+ loop do
519
+ input = STDIN.getch.downcase
520
+
521
+ case input
522
+ when 'y'
523
+ confirmed = true
524
+ break
525
+ when 'n', "\e" # n or ESC
526
+ confirmed = false
527
+ break
528
+ end
529
+ end
530
+
531
+ @dialog_renderer.clear_area(x, y, width, height)
532
+ @terminal_ui&.refresh_display
533
+
534
+ return false unless confirmed
535
+
536
+ # FileOperationsを使用して削除
537
+ result = @file_operations.delete([current_name], current_path)
538
+
539
+ # ディレクトリ表示を更新
540
+ if result.success
541
+ @directory_listing.refresh
542
+
543
+ # カーソル位置を調整
544
+ entries = @directory_listing.list_entries
545
+ @current_index = [@current_index, entries.length - 1].min if @current_index >= entries.length
546
+ @current_index = 0 if @current_index < 0
547
+ end
548
+
549
+ @terminal_ui&.refresh_display
428
550
  result.success
429
551
  end
430
552
 
@@ -713,6 +835,10 @@ module Rufio
713
835
  else
714
836
  false
715
837
  end
838
+ when :rename
839
+ @bookmark_manager.rename_interactive
840
+ @terminal_ui&.refresh_display
841
+ true
716
842
  when :remove
717
843
  @bookmark_manager.remove_interactive
718
844
  @terminal_ui&.refresh_display
@@ -740,6 +866,61 @@ module Rufio
740
866
  end
741
867
  end
742
868
 
869
+ def add_bookmark
870
+ current_path = @directory_listing&.current_path || Dir.pwd
871
+
872
+ # カレントディレクトリが既にブックマークされているかチェック
873
+ bookmarks = @bookmark_manager.list
874
+ existing = bookmarks.find { |b| b[:path] == current_path }
875
+
876
+ if existing
877
+ # 既に存在する場合はメッセージを表示して終了
878
+ content_lines = [
879
+ '',
880
+ 'This directory is already bookmarked',
881
+ "Name: #{existing[:name]}",
882
+ '',
883
+ 'Press any key to continue...'
884
+ ]
885
+
886
+ title = 'Bookmark Exists'
887
+ width = 50
888
+ height = content_lines.length + 4
889
+ x, y = @dialog_renderer.calculate_center(width, height)
890
+
891
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
892
+ border_color: "\e[33m", # Yellow
893
+ title_color: "\e[1;33m", # Bold yellow
894
+ content_color: "\e[37m" # White
895
+ })
896
+
897
+ STDIN.getch
898
+ @dialog_renderer.clear_area(x, y, width, height)
899
+ @terminal_ui&.refresh_display
900
+ return false
901
+ end
902
+
903
+ # ディレクトリ名を取得
904
+ dir_name = File.basename(current_path)
905
+
906
+ # ダイアログレンダラーを使用して入力ダイアログを表示
907
+ title = "Add Bookmark: #{dir_name}"
908
+ prompt = "Enter bookmark name:"
909
+ bookmark_name = @dialog_renderer.show_input_dialog(title, prompt, {
910
+ border_color: "\e[32m", # Green
911
+ title_color: "\e[1;32m", # Bold green
912
+ content_color: "\e[37m" # White
913
+ })
914
+
915
+ return false if bookmark_name.nil? || bookmark_name.empty?
916
+
917
+ # ブックマークを追加
918
+ result = @bookmark_manager.add(current_path, bookmark_name)
919
+
920
+ @terminal_ui&.refresh_display if @terminal_ui
921
+ result
922
+ end
923
+
743
924
  # ヘルパーメソッド
744
925
  def wait_for_keypress
745
926
  print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
@@ -786,8 +967,6 @@ module Rufio
786
967
  true
787
968
  end
788
969
 
789
- private
790
-
791
970
  # カーソルを画面下部の入力行に移動
792
971
  def move_to_input_line
793
972
  # 画面の最終行にカーソルを移動
@@ -833,5 +1012,236 @@ module Rufio
833
1012
  end
834
1013
  end
835
1014
  end
1015
+
1016
+ # プロジェクトモード中のキー処理
1017
+ def handle_project_mode_key(key)
1018
+ case key
1019
+ when "\e" # ESC - ログモードならプロジェクトモードに戻る、そうでなければ終了
1020
+ if @in_log_mode
1021
+ exit_log_mode
1022
+ else
1023
+ exit_project_mode
1024
+ end
1025
+ when ' ' # Space - ブックマークを選択
1026
+ if @in_log_mode
1027
+ # ログモード中は何もしない
1028
+ false
1029
+ else
1030
+ select_bookmark_in_project_mode
1031
+ end
1032
+ when 'l' # l - ログディレクトリに移動
1033
+ if @in_log_mode
1034
+ # すでにログモードの場合は何もしない
1035
+ false
1036
+ else
1037
+ @in_log_mode = true
1038
+ @current_index = 0 # ログモードに入るときインデックスをリセット
1039
+ @terminal_ui&.enter_log_mode(@project_log) if @terminal_ui
1040
+ true
1041
+ end
1042
+ when 'j' # 下に移動
1043
+ move_down_in_project_mode
1044
+ when 'k' # 上に移動
1045
+ move_up_in_project_mode
1046
+ when 'g' # 先頭に移動
1047
+ @current_index = 0
1048
+ true
1049
+ when 'G' # 末尾に移動
1050
+ entries = get_project_mode_entries
1051
+ @current_index = [entries.length - 1, 0].max
1052
+ true
1053
+ when ':' # コマンドモード
1054
+ activate_project_command_mode
1055
+ when 'r' # r - ブックマークをリネーム
1056
+ if @in_log_mode
1057
+ false
1058
+ else
1059
+ rename_bookmark_in_project_mode
1060
+ end
1061
+ when 'd' # d - ブックマークを削除
1062
+ if @in_log_mode
1063
+ false
1064
+ else
1065
+ delete_bookmark_in_project_mode
1066
+ end
1067
+ else
1068
+ false
1069
+ end
1070
+ end
1071
+
1072
+ # プロジェクトモード用のエントリ数取得
1073
+ def get_project_mode_entries
1074
+ if @in_log_mode
1075
+ @project_log.list_log_files
1076
+ else
1077
+ @project_mode.list_bookmarks
1078
+ end
1079
+ end
1080
+
1081
+ # プロジェクトモード用の下移動
1082
+ def move_down_in_project_mode
1083
+ entries = get_project_mode_entries
1084
+ @current_index = [@current_index + 1, entries.length - 1].min
1085
+ true
1086
+ end
1087
+
1088
+ # プロジェクトモード用の上移動
1089
+ def move_up_in_project_mode
1090
+ @current_index = [@current_index - 1, 0].max
1091
+ true
1092
+ end
1093
+
1094
+ # プロジェクトモードに入る
1095
+ def enter_project_mode
1096
+ @project_mode.activate
1097
+ @in_project_mode = true
1098
+ @current_index = 0 # プロジェクトモードに入るときインデックスをリセット
1099
+ @terminal_ui&.set_project_mode(@project_mode, @project_command, @project_log) if @terminal_ui
1100
+ true
1101
+ end
1102
+
1103
+ # プロジェクトモードを終了
1104
+ def exit_project_mode
1105
+ @project_mode.deactivate
1106
+ @in_project_mode = false
1107
+ @in_log_mode = false
1108
+ @current_index = 0 # 通常モードに戻るときインデックスをリセット
1109
+ @terminal_ui&.exit_project_mode if @terminal_ui
1110
+ refresh
1111
+ true
1112
+ end
1113
+
1114
+ # プロジェクトモードでコマンドモードを起動
1115
+ def activate_project_command_mode
1116
+ # 選択されたプロジェクトがあるかチェック
1117
+ if @project_mode.selected_path.nil?
1118
+ @terminal_ui&.show_project_not_selected_message if @terminal_ui
1119
+ return false
1120
+ end
1121
+
1122
+ @terminal_ui&.activate_project_command_mode(@project_mode, @project_command, @project_log) if @terminal_ui
1123
+ true
1124
+ end
1125
+
1126
+ # ログモードを終了してプロジェクトモードに戻る
1127
+ def exit_log_mode
1128
+ @in_log_mode = false
1129
+ @current_index = 0 # プロジェクトモードに戻るときインデックスをリセット
1130
+ @terminal_ui&.exit_log_mode if @terminal_ui
1131
+ true
1132
+ end
1133
+
1134
+ # プロジェクトモードでブックマークをリネーム
1135
+ def rename_bookmark_in_project_mode
1136
+ bookmarks = @bookmark_manager.list
1137
+ return false if bookmarks.empty? || @current_index >= bookmarks.length
1138
+
1139
+ current_bookmark = bookmarks[@current_index]
1140
+ old_name = current_bookmark[:name]
1141
+
1142
+ # ダイアログレンダラーを使用して入力ダイアログを表示
1143
+ new_name = @dialog_renderer.show_input_dialog("Rename: #{old_name}", "Enter new name:", {
1144
+ border_color: "\e[33m", # Yellow
1145
+ title_color: "\e[1;33m", # Bold yellow
1146
+ content_color: "\e[37m" # White
1147
+ })
1148
+
1149
+ return false if new_name.nil? || new_name.empty?
1150
+
1151
+ # Bookmarkを使用してリネーム
1152
+ result = @bookmark_manager.instance_variable_get(:@bookmark).rename(old_name, new_name)
1153
+
1154
+ @terminal_ui&.refresh_display if @terminal_ui
1155
+ result
1156
+ end
1157
+
1158
+ # プロジェクトモードでブックマークを削除
1159
+ def delete_bookmark_in_project_mode
1160
+ bookmarks = @bookmark_manager.list
1161
+ return false if bookmarks.empty? || @current_index >= bookmarks.length
1162
+
1163
+ current_bookmark = bookmarks[@current_index]
1164
+ bookmark_name = current_bookmark[:name]
1165
+
1166
+ # 確認ダイアログを表示
1167
+ content_lines = [
1168
+ '',
1169
+ "Delete this bookmark?",
1170
+ " #{bookmark_name}",
1171
+ '',
1172
+ ' [Y]es - Delete',
1173
+ ' [N]o - Cancel',
1174
+ ''
1175
+ ]
1176
+
1177
+ title = 'Confirm Delete'
1178
+ width = [50, bookmark_name.length + 10].max
1179
+ height = content_lines.length + 4
1180
+ x, y = @dialog_renderer.calculate_center(width, height)
1181
+
1182
+ @dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
1183
+ border_color: "\e[31m", # Red (warning)
1184
+ title_color: "\e[1;31m", # Bold red
1185
+ content_color: "\e[37m" # White
1186
+ })
1187
+
1188
+ # 確認を待つ
1189
+ confirmed = false
1190
+ loop do
1191
+ input = STDIN.getch.downcase
1192
+
1193
+ case input
1194
+ when 'y'
1195
+ confirmed = true
1196
+ break
1197
+ when 'n', "\e" # n or ESC
1198
+ confirmed = false
1199
+ break
1200
+ end
1201
+ end
1202
+
1203
+ @dialog_renderer.clear_area(x, y, width, height)
1204
+ @terminal_ui&.refresh_display
1205
+
1206
+ return false unless confirmed
1207
+
1208
+ # Bookmarkを使用して削除
1209
+ result = @bookmark_manager.instance_variable_get(:@bookmark).remove(bookmark_name)
1210
+
1211
+ # 削除後、選択されていたブックマークがなくなった場合は選択をクリア
1212
+ if result && @project_mode.selected_path == current_bookmark[:path]
1213
+ @project_mode.clear_selection
1214
+ end
1215
+
1216
+ # カーソル位置を調整
1217
+ if result
1218
+ bookmarks_after = @bookmark_manager.list
1219
+ @current_index = [@current_index, bookmarks_after.length - 1].min if @current_index >= bookmarks_after.length
1220
+ @current_index = 0 if @current_index < 0
1221
+ end
1222
+
1223
+ @terminal_ui&.refresh_display if @terminal_ui
1224
+ result
1225
+ end
1226
+
1227
+ # プロジェクトモードでブックマークの選択をトグル
1228
+ def select_bookmark_in_project_mode
1229
+ bookmarks = @bookmark_manager.list
1230
+ return false if bookmarks.empty? || @current_index >= bookmarks.length
1231
+
1232
+ # ブックマークをプロジェクトとして選択
1233
+ @project_mode.select_bookmark(@current_index + 1)
1234
+ true
1235
+ end
1236
+
1237
+ # プロジェクトモード中かどうか
1238
+ def in_project_mode?
1239
+ @in_project_mode
1240
+ end
1241
+
1242
+ # ログモード中かどうか
1243
+ def in_log_mode?
1244
+ @in_log_mode
1245
+ end
836
1246
  end
837
1247
  end