beniya 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +99 -0
- data/CHANGELOG_v0.4.0.md +146 -0
- data/README.md +62 -35
- data/README_EN.md +63 -35
- data/lib/beniya/application.rb +1 -0
- data/lib/beniya/file_opener.rb +3 -3
- data/lib/beniya/file_preview.rb +23 -3
- data/lib/beniya/keybind_handler.rb +502 -65
- data/lib/beniya/terminal_ui.rb +55 -9
- data/lib/beniya/version.rb +1 -1
- data/publish_gem.zsh +131 -0
- data/test_delete/test1.txt +1 -0
- data/test_delete/test2.txt +1 -0
- metadata +7 -2
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Beniya
|
4
4
|
class KeybindHandler
|
5
|
-
attr_reader :current_index
|
5
|
+
attr_reader :current_index, :filter_query
|
6
6
|
|
7
7
|
def initialize
|
8
8
|
@current_index = 0
|
@@ -10,9 +10,11 @@ module Beniya
|
|
10
10
|
@terminal_ui = nil
|
11
11
|
@file_opener = FileOpener.new
|
12
12
|
@filter_mode = false
|
13
|
-
@filter_query =
|
13
|
+
@filter_query = ''
|
14
14
|
@filtered_entries = []
|
15
15
|
@original_entries = []
|
16
|
+
@selected_items = []
|
17
|
+
@base_directory = nil
|
16
18
|
end
|
17
19
|
|
18
20
|
def set_directory_listing(directory_listing)
|
@@ -24,13 +26,23 @@ module Beniya
|
|
24
26
|
@terminal_ui = terminal_ui
|
25
27
|
end
|
26
28
|
|
29
|
+
def set_base_directory(base_dir)
|
30
|
+
@base_directory = File.expand_path(base_dir)
|
31
|
+
end
|
32
|
+
|
33
|
+
def selected_items
|
34
|
+
@selected_items.dup
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_selected?(entry_name)
|
38
|
+
@selected_items.include?(entry_name)
|
39
|
+
end
|
40
|
+
|
27
41
|
def handle_key(key)
|
28
42
|
return false unless @directory_listing
|
29
43
|
|
30
44
|
# フィルターモード中は他のキーバインドを無効化
|
31
|
-
if @filter_mode
|
32
|
-
return handle_filter_input(key)
|
33
|
-
end
|
45
|
+
return handle_filter_input(key) if @filter_mode
|
34
46
|
|
35
47
|
case key
|
36
48
|
when 'j'
|
@@ -39,7 +51,7 @@ module Beniya
|
|
39
51
|
move_up
|
40
52
|
when 'h'
|
41
53
|
navigate_parent
|
42
|
-
when 'l', "\r", "\n"
|
54
|
+
when 'l', "\r", "\n" # l, Enter
|
43
55
|
navigate_enter
|
44
56
|
when 'g'
|
45
57
|
move_to_top
|
@@ -60,9 +72,9 @@ module Beniya
|
|
60
72
|
# 新規フィルターモード開始
|
61
73
|
start_filter_mode
|
62
74
|
end
|
63
|
-
when ' '
|
64
|
-
|
65
|
-
when "\e"
|
75
|
+
when ' ' # Space - toggle selection
|
76
|
+
toggle_selection
|
77
|
+
when "\e" # ESC
|
66
78
|
if !@filter_query.empty?
|
67
79
|
# フィルタが設定されている場合はクリア
|
68
80
|
clear_filter_mode
|
@@ -82,8 +94,14 @@ module Beniya
|
|
82
94
|
create_file
|
83
95
|
when 'A' # A
|
84
96
|
create_directory
|
97
|
+
when 'm' # m - move selected files to base directory
|
98
|
+
move_selected_to_base
|
99
|
+
when 'p' # p - copy selected files to base directory
|
100
|
+
copy_selected_to_base
|
101
|
+
when 'x' # x - delete selected files
|
102
|
+
delete_selected_files
|
85
103
|
else
|
86
|
-
false
|
104
|
+
false # #{ConfigLoader.message('keybind.invalid_key')}
|
87
105
|
end
|
88
106
|
end
|
89
107
|
|
@@ -101,10 +119,6 @@ module Beniya
|
|
101
119
|
@filter_mode || !@filter_query.empty?
|
102
120
|
end
|
103
121
|
|
104
|
-
def filter_query
|
105
|
-
@filter_query
|
106
|
-
end
|
107
|
-
|
108
122
|
def get_active_entries
|
109
123
|
if @filter_mode || !@filter_query.empty?
|
110
124
|
@filtered_entries.empty? ? [] : @filtered_entries
|
@@ -141,7 +155,7 @@ module Beniya
|
|
141
155
|
entry = current_entry
|
142
156
|
return false unless entry
|
143
157
|
|
144
|
-
if entry[:type] ==
|
158
|
+
if entry[:type] == 'directory'
|
145
159
|
result = @directory_listing.navigate_to(entry[:name])
|
146
160
|
if result
|
147
161
|
@current_index = 0 # select first entry in new directory
|
@@ -166,7 +180,7 @@ module Beniya
|
|
166
180
|
def refresh
|
167
181
|
# ウィンドウサイズを更新して画面を再描画
|
168
182
|
@terminal_ui&.refresh_display
|
169
|
-
|
183
|
+
|
170
184
|
@directory_listing.refresh
|
171
185
|
if @filter_mode || !@filter_query.empty?
|
172
186
|
# Re-apply filter with new directory contents
|
@@ -183,8 +197,8 @@ module Beniya
|
|
183
197
|
def open_current_file
|
184
198
|
entry = current_entry
|
185
199
|
return false unless entry
|
186
|
-
|
187
|
-
if entry[:type] ==
|
200
|
+
|
201
|
+
if entry[:type] == 'file'
|
188
202
|
@file_opener.open_file(entry[:path])
|
189
203
|
true
|
190
204
|
else
|
@@ -199,78 +213,76 @@ module Beniya
|
|
199
213
|
end
|
200
214
|
|
201
215
|
def exit_request
|
202
|
-
true
|
216
|
+
true # request exit
|
203
217
|
end
|
204
218
|
|
205
219
|
def fzf_search
|
206
220
|
return false unless fzf_available?
|
207
|
-
|
221
|
+
|
208
222
|
current_path = @directory_listing&.current_path || Dir.pwd
|
209
|
-
|
223
|
+
|
210
224
|
# fzfでファイル検索を実行
|
211
225
|
selected_file = `cd "#{current_path}" && find . -type f | fzf --preview 'cat {}'`.strip
|
212
|
-
|
226
|
+
|
213
227
|
# ファイルが選択された場合、そのファイルを開く
|
214
228
|
if !selected_file.empty? && File.exist?(File.join(current_path, selected_file))
|
215
229
|
full_path = File.expand_path(selected_file, current_path)
|
216
230
|
@file_opener.open_file(full_path)
|
217
231
|
end
|
218
|
-
|
232
|
+
|
219
233
|
true
|
220
234
|
end
|
221
235
|
|
222
236
|
def fzf_available?
|
223
|
-
system(
|
237
|
+
system('which fzf > /dev/null 2>&1')
|
224
238
|
end
|
225
239
|
|
226
240
|
def rga_search
|
227
241
|
return false unless rga_available?
|
228
|
-
|
242
|
+
|
229
243
|
current_path = @directory_listing&.current_path || Dir.pwd
|
230
|
-
|
244
|
+
|
231
245
|
# input search keyword
|
232
246
|
print ConfigLoader.message('keybind.search_text')
|
233
247
|
search_query = STDIN.gets.chomp
|
234
248
|
return false if search_query.empty?
|
235
|
-
|
249
|
+
|
236
250
|
# execute rga file content search
|
237
251
|
search_results = `cd "#{current_path}" && rga --line-number --with-filename "#{search_query}" . 2>/dev/null`
|
238
|
-
|
252
|
+
|
239
253
|
if search_results.empty?
|
240
254
|
puts "\n#{ConfigLoader.message('keybind.no_matches')}"
|
241
255
|
print ConfigLoader.message('keybind.press_any_key')
|
242
256
|
STDIN.getch
|
243
257
|
return true
|
244
258
|
end
|
245
|
-
|
259
|
+
|
246
260
|
# pass results to fzf for selection
|
247
|
-
selected_result = IO.popen(
|
261
|
+
selected_result = IO.popen('fzf', 'r+') do |fzf|
|
248
262
|
fzf.write(search_results)
|
249
263
|
fzf.close_write
|
250
264
|
fzf.read.strip
|
251
265
|
end
|
252
|
-
|
266
|
+
|
253
267
|
# extract file path and line number from selected result
|
254
268
|
if !selected_result.empty? && selected_result.match(/^(.+?):(\d+):/)
|
255
|
-
file_path =
|
256
|
-
line_number =
|
269
|
+
file_path = ::Regexp.last_match(1)
|
270
|
+
line_number = ::Regexp.last_match(2).to_i
|
257
271
|
full_path = File.expand_path(file_path, current_path)
|
258
|
-
|
259
|
-
if File.exist?(full_path)
|
260
|
-
@file_opener.open_file_with_line(full_path, line_number)
|
261
|
-
end
|
272
|
+
|
273
|
+
@file_opener.open_file_with_line(full_path, line_number) if File.exist?(full_path)
|
262
274
|
end
|
263
|
-
|
275
|
+
|
264
276
|
true
|
265
277
|
end
|
266
278
|
|
267
279
|
def rga_available?
|
268
|
-
system(
|
280
|
+
system('which rga > /dev/null 2>&1')
|
269
281
|
end
|
270
282
|
|
271
283
|
def start_filter_mode
|
272
284
|
@filter_mode = true
|
273
|
-
@filter_query =
|
285
|
+
@filter_query = ''
|
274
286
|
@original_entries = @directory_listing.list_entries.dup
|
275
287
|
@filtered_entries = @original_entries.dup
|
276
288
|
@current_index = 0
|
@@ -279,11 +291,11 @@ module Beniya
|
|
279
291
|
|
280
292
|
def handle_filter_input(key)
|
281
293
|
case key
|
282
|
-
when "\e"
|
294
|
+
when "\e" # ESC - フィルタをクリアして通常モードに戻る
|
283
295
|
clear_filter_mode
|
284
|
-
when "\r", "\n"
|
296
|
+
when "\r", "\n" # Enter - フィルタを維持して通常モードに戻る
|
285
297
|
exit_filter_mode_keep_filter
|
286
|
-
when "\u007f", "\b"
|
298
|
+
when "\u007f", "\b" # Backspace
|
287
299
|
if @filter_query.length > 0
|
288
300
|
@filter_query = @filter_query[0...-1]
|
289
301
|
apply_filter
|
@@ -292,10 +304,10 @@ module Beniya
|
|
292
304
|
end
|
293
305
|
else
|
294
306
|
# printable characters (英数字、記号、日本語文字など)
|
295
|
-
if key.length == 1 && key.ord >= 32 && key.ord < 127
|
307
|
+
if key.length == 1 && key.ord >= 32 && key.ord < 127 # ASCII printable
|
296
308
|
@filter_query += key
|
297
309
|
apply_filter
|
298
|
-
elsif key.bytesize > 1
|
310
|
+
elsif key.bytesize > 1 # Multi-byte characters (Japanese, etc.)
|
299
311
|
@filter_query += key
|
300
312
|
apply_filter
|
301
313
|
end
|
@@ -325,7 +337,7 @@ module Beniya
|
|
325
337
|
def clear_filter_mode
|
326
338
|
# フィルタをクリアして通常モードに戻る
|
327
339
|
@filter_mode = false
|
328
|
-
@filter_query =
|
340
|
+
@filter_query = ''
|
329
341
|
@filtered_entries = []
|
330
342
|
@original_entries = []
|
331
343
|
@current_index = 0
|
@@ -338,12 +350,12 @@ module Beniya
|
|
338
350
|
|
339
351
|
def create_file
|
340
352
|
current_path = @directory_listing&.current_path || Dir.pwd
|
341
|
-
|
353
|
+
|
342
354
|
# ファイル名の入力を求める
|
343
355
|
print ConfigLoader.message('keybind.input_filename')
|
344
356
|
filename = STDIN.gets.chomp
|
345
357
|
return false if filename.empty?
|
346
|
-
|
358
|
+
|
347
359
|
# 不正なファイル名のチェック
|
348
360
|
if filename.include?('/') || filename.include?('\\')
|
349
361
|
puts "\n#{ConfigLoader.message('keybind.invalid_filename')}"
|
@@ -351,9 +363,9 @@ module Beniya
|
|
351
363
|
STDIN.getch
|
352
364
|
return false
|
353
365
|
end
|
354
|
-
|
366
|
+
|
355
367
|
file_path = File.join(current_path, filename)
|
356
|
-
|
368
|
+
|
357
369
|
# ファイルが既に存在する場合の確認
|
358
370
|
if File.exist?(file_path)
|
359
371
|
puts "\n#{ConfigLoader.message('keybind.file_exists')}"
|
@@ -361,24 +373,24 @@ module Beniya
|
|
361
373
|
STDIN.getch
|
362
374
|
return false
|
363
375
|
end
|
364
|
-
|
376
|
+
|
365
377
|
begin
|
366
378
|
# ファイルを作成
|
367
379
|
File.write(file_path, '')
|
368
|
-
|
380
|
+
|
369
381
|
# ディレクトリ表示を更新
|
370
382
|
@directory_listing.refresh
|
371
|
-
|
383
|
+
|
372
384
|
# 作成したファイルを選択状態にする
|
373
385
|
entries = @directory_listing.list_entries
|
374
386
|
new_file_index = entries.find_index { |entry| entry[:name] == filename }
|
375
387
|
@current_index = new_file_index if new_file_index
|
376
|
-
|
388
|
+
|
377
389
|
puts "\n#{ConfigLoader.message('keybind.file_created')}: #{filename}"
|
378
390
|
print ConfigLoader.message('keybind.press_any_key')
|
379
391
|
STDIN.getch
|
380
392
|
true
|
381
|
-
rescue => e
|
393
|
+
rescue StandardError => e
|
382
394
|
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
383
395
|
print ConfigLoader.message('keybind.press_any_key')
|
384
396
|
STDIN.getch
|
@@ -388,12 +400,12 @@ module Beniya
|
|
388
400
|
|
389
401
|
def create_directory
|
390
402
|
current_path = @directory_listing&.current_path || Dir.pwd
|
391
|
-
|
403
|
+
|
392
404
|
# ディレクトリ名の入力を求める
|
393
405
|
print ConfigLoader.message('keybind.input_dirname')
|
394
406
|
dirname = STDIN.gets.chomp
|
395
407
|
return false if dirname.empty?
|
396
|
-
|
408
|
+
|
397
409
|
# 不正なディレクトリ名のチェック
|
398
410
|
if dirname.include?('/') || dirname.include?('\\')
|
399
411
|
puts "\n#{ConfigLoader.message('keybind.invalid_dirname')}"
|
@@ -401,9 +413,9 @@ module Beniya
|
|
401
413
|
STDIN.getch
|
402
414
|
return false
|
403
415
|
end
|
404
|
-
|
416
|
+
|
405
417
|
dir_path = File.join(current_path, dirname)
|
406
|
-
|
418
|
+
|
407
419
|
# ディレクトリが既に存在する場合の確認
|
408
420
|
if File.exist?(dir_path)
|
409
421
|
puts "\n#{ConfigLoader.message('keybind.directory_exists')}"
|
@@ -411,29 +423,454 @@ module Beniya
|
|
411
423
|
STDIN.getch
|
412
424
|
return false
|
413
425
|
end
|
414
|
-
|
426
|
+
|
415
427
|
begin
|
416
428
|
# ディレクトリを作成
|
417
429
|
Dir.mkdir(dir_path)
|
418
|
-
|
430
|
+
|
419
431
|
# ディレクトリ表示を更新
|
420
432
|
@directory_listing.refresh
|
421
|
-
|
433
|
+
|
422
434
|
# 作成したディレクトリを選択状態にする
|
423
435
|
entries = @directory_listing.list_entries
|
424
436
|
new_dir_index = entries.find_index { |entry| entry[:name] == dirname }
|
425
437
|
@current_index = new_dir_index if new_dir_index
|
426
|
-
|
438
|
+
|
427
439
|
puts "\n#{ConfigLoader.message('keybind.directory_created')}: #{dirname}"
|
428
440
|
print ConfigLoader.message('keybind.press_any_key')
|
429
441
|
STDIN.getch
|
430
442
|
true
|
431
|
-
rescue => e
|
443
|
+
rescue StandardError => e
|
432
444
|
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
433
445
|
print ConfigLoader.message('keybind.press_any_key')
|
434
446
|
STDIN.getch
|
435
447
|
false
|
436
448
|
end
|
437
449
|
end
|
450
|
+
|
451
|
+
def toggle_selection
|
452
|
+
entry = current_entry
|
453
|
+
return false unless entry
|
454
|
+
|
455
|
+
if @selected_items.include?(entry[:name])
|
456
|
+
@selected_items.delete(entry[:name])
|
457
|
+
else
|
458
|
+
@selected_items << entry[:name]
|
459
|
+
end
|
460
|
+
true
|
461
|
+
end
|
462
|
+
|
463
|
+
def move_selected_to_base
|
464
|
+
return false if @selected_items.empty? || @base_directory.nil?
|
465
|
+
|
466
|
+
if show_confirmation_dialog('Move', @selected_items.length)
|
467
|
+
perform_file_operation(:move, @selected_items, @base_directory)
|
468
|
+
else
|
469
|
+
false
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def copy_selected_to_base
|
474
|
+
return false if @selected_items.empty? || @base_directory.nil?
|
475
|
+
|
476
|
+
if show_confirmation_dialog('Copy', @selected_items.length)
|
477
|
+
perform_file_operation(:copy, @selected_items, @base_directory)
|
478
|
+
else
|
479
|
+
false
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
def show_confirmation_dialog(operation, count)
|
484
|
+
print "\n#{operation} #{count} item(s)? (y/n): "
|
485
|
+
response = STDIN.gets.chomp.downcase
|
486
|
+
%w[y yes].include?(response)
|
487
|
+
end
|
488
|
+
|
489
|
+
def perform_file_operation(operation, items, destination)
|
490
|
+
success_count = 0
|
491
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
492
|
+
|
493
|
+
items.each do |item_name|
|
494
|
+
source_path = File.join(current_path, item_name)
|
495
|
+
dest_path = File.join(destination, item_name)
|
496
|
+
|
497
|
+
begin
|
498
|
+
case operation
|
499
|
+
when :move
|
500
|
+
if File.exist?(dest_path)
|
501
|
+
puts "\n#{item_name} already exists in destination. Skipping."
|
502
|
+
next
|
503
|
+
end
|
504
|
+
FileUtils.mv(source_path, dest_path)
|
505
|
+
when :copy
|
506
|
+
if File.exist?(dest_path)
|
507
|
+
puts "\n#{item_name} already exists in destination. Skipping."
|
508
|
+
next
|
509
|
+
end
|
510
|
+
if File.directory?(source_path)
|
511
|
+
FileUtils.cp_r(source_path, dest_path)
|
512
|
+
else
|
513
|
+
FileUtils.cp(source_path, dest_path)
|
514
|
+
end
|
515
|
+
end
|
516
|
+
success_count += 1
|
517
|
+
rescue StandardError => e
|
518
|
+
puts "\nFailed to #{operation == :move ? 'move' : 'copy'} #{item_name}: #{e.message}"
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# 操作完了後の処理
|
523
|
+
@selected_items.clear
|
524
|
+
@directory_listing.refresh if @directory_listing
|
525
|
+
|
526
|
+
puts "\n#{operation == :move ? 'Moved' : 'Copied'} #{success_count} item(s)."
|
527
|
+
print 'Press any key to continue...'
|
528
|
+
STDIN.getch
|
529
|
+
true
|
530
|
+
end
|
531
|
+
|
532
|
+
def delete_selected_files
|
533
|
+
return false if @selected_items.empty?
|
534
|
+
|
535
|
+
if show_delete_confirmation(@selected_items.length)
|
536
|
+
perform_delete_operation(@selected_items)
|
537
|
+
else
|
538
|
+
false
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
def show_delete_confirmation(count)
|
543
|
+
show_floating_delete_confirmation(count)
|
544
|
+
end
|
545
|
+
|
546
|
+
def show_floating_delete_confirmation(count)
|
547
|
+
# コンテンツの準備
|
548
|
+
title = 'Delete Confirmation'
|
549
|
+
content_lines = [
|
550
|
+
'',
|
551
|
+
"Delete #{count} item(s)?",
|
552
|
+
'',
|
553
|
+
' [Y]es - Delete',
|
554
|
+
' [N]o - Cancel',
|
555
|
+
''
|
556
|
+
]
|
557
|
+
|
558
|
+
# ダイアログのサイズ設定(コンテンツに合わせて調整)
|
559
|
+
dialog_width = 45
|
560
|
+
# タイトルあり: 上枠1 + タイトル1 + 区切り1 + コンテンツ6 + 下枠1 = 10
|
561
|
+
dialog_height = 4 + content_lines.length
|
562
|
+
|
563
|
+
# ダイアログの位置を中央に設定
|
564
|
+
x, y = get_screen_center(dialog_width, dialog_height)
|
565
|
+
|
566
|
+
# ダイアログの描画
|
567
|
+
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
568
|
+
border_color: "\e[31m", # 赤色(警告)
|
569
|
+
title_color: "\e[1;31m", # 太字赤色
|
570
|
+
content_color: "\e[37m" # 白色
|
571
|
+
})
|
572
|
+
|
573
|
+
# フラッシュしてユーザーの注意を引く
|
574
|
+
print "\a" # ベル音
|
575
|
+
|
576
|
+
# キー入力待機
|
577
|
+
loop do
|
578
|
+
input = STDIN.getch.downcase
|
579
|
+
|
580
|
+
case input
|
581
|
+
when 'y'
|
582
|
+
# ダイアログをクリア
|
583
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
584
|
+
@terminal_ui&.refresh_display # 画面を再描画
|
585
|
+
return true
|
586
|
+
when 'n', "\e", "\x03" # n, ESC, Ctrl+C
|
587
|
+
# ダイアログをクリア
|
588
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
589
|
+
@terminal_ui&.refresh_display # 画面を再描画
|
590
|
+
return false
|
591
|
+
when 'q' # qキーでもキャンセル
|
592
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
593
|
+
@terminal_ui&.refresh_display
|
594
|
+
return false
|
595
|
+
end
|
596
|
+
# 無効なキー入力の場合は再度ループ
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
def perform_delete_operation(items)
|
601
|
+
success_count = 0
|
602
|
+
error_messages = []
|
603
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
604
|
+
debug_log = []
|
605
|
+
|
606
|
+
items.each do |item_name|
|
607
|
+
item_path = File.join(current_path, item_name)
|
608
|
+
debug_log << "Processing: #{item_name}"
|
609
|
+
|
610
|
+
begin
|
611
|
+
# ファイル/ディレクトリの存在確認
|
612
|
+
unless File.exist?(item_path)
|
613
|
+
error_messages << "#{item_name}: File not found"
|
614
|
+
debug_log << ' Error: File not found'
|
615
|
+
next
|
616
|
+
end
|
617
|
+
|
618
|
+
debug_log << ' Existence check: OK'
|
619
|
+
is_directory = File.directory?(item_path)
|
620
|
+
debug_log << " Type: #{is_directory ? 'Directory' : 'File'}"
|
621
|
+
|
622
|
+
if is_directory
|
623
|
+
FileUtils.rm_rf(item_path)
|
624
|
+
debug_log << ' FileUtils.rm_rf executed'
|
625
|
+
else
|
626
|
+
FileUtils.rm(item_path)
|
627
|
+
debug_log << ' FileUtils.rm executed'
|
628
|
+
end
|
629
|
+
|
630
|
+
# 削除が実際に成功したかを確認
|
631
|
+
sleep(0.01) # 10ms待機してファイルシステムの同期を待つ
|
632
|
+
still_exists = File.exist?(item_path)
|
633
|
+
debug_log << " Post-deletion check: #{still_exists}"
|
634
|
+
|
635
|
+
if still_exists
|
636
|
+
error_messages << "#{item_name}: Deletion failed"
|
637
|
+
debug_log << ' Result: Failed'
|
638
|
+
else
|
639
|
+
success_count += 1
|
640
|
+
debug_log << ' Result: Success'
|
641
|
+
end
|
642
|
+
rescue StandardError => e
|
643
|
+
error_messages << "#{item_name}: #{e.message}"
|
644
|
+
debug_log << " Exception: #{e.message}"
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
# デバッグログをファイルに出力(開発時のみ)
|
649
|
+
if ENV['BENIYA_DEBUG'] == '1'
|
650
|
+
debug_file = File.join(Dir.home, '.beniya_delete_debug.log')
|
651
|
+
File.open(debug_file, 'a') do |f|
|
652
|
+
f.puts "=== Delete Process Debug #{Time.now} ==="
|
653
|
+
f.puts "Target directory: #{current_path}"
|
654
|
+
f.puts "Target items: #{items.inspect}"
|
655
|
+
debug_log.each { |line| f.puts line }
|
656
|
+
f.puts "Final result: #{success_count} successful, #{items.length - success_count} failed"
|
657
|
+
f.puts "Error messages: #{error_messages.inspect}"
|
658
|
+
f.puts ''
|
659
|
+
end
|
660
|
+
end
|
661
|
+
|
662
|
+
|
663
|
+
# デバッグ用:削除結果の値をログファイルに出力
|
664
|
+
result_debug_file = File.join(Dir.home, '.beniya_result_debug.log')
|
665
|
+
File.open(result_debug_file, 'a') do |f|
|
666
|
+
f.puts "=== Delete Result Debug #{Time.now} ==="
|
667
|
+
f.puts "success_count: #{success_count}"
|
668
|
+
f.puts "total_count: #{items.length}"
|
669
|
+
f.puts "error_messages.length: #{error_messages.length}"
|
670
|
+
f.puts "has_errors: #{!error_messages.empty?}"
|
671
|
+
f.puts "condition check: success_count == total_count && !has_errors = #{success_count == items.length && error_messages.empty?}"
|
672
|
+
f.puts ""
|
673
|
+
end
|
674
|
+
|
675
|
+
# 削除結果をフローティングウィンドウで表示
|
676
|
+
show_deletion_result(success_count, items.length, error_messages)
|
677
|
+
|
678
|
+
# 削除完了後の処理
|
679
|
+
@selected_items.clear
|
680
|
+
@directory_listing.refresh if @directory_listing
|
681
|
+
|
682
|
+
true
|
683
|
+
end
|
684
|
+
|
685
|
+
def show_deletion_result(success_count, total_count, error_messages = [])
|
686
|
+
# 詳細デバッグログを出力
|
687
|
+
detailed_debug_file = File.join(Dir.home, '.beniya_detailed_debug.log')
|
688
|
+
File.open(detailed_debug_file, 'a') do |f|
|
689
|
+
f.puts "=== show_deletion_result called #{Time.now} ==="
|
690
|
+
f.puts "Arguments: success_count=#{success_count}, total_count=#{total_count}"
|
691
|
+
f.puts "error_messages: #{error_messages.inspect}"
|
692
|
+
f.puts "error_messages.empty?: #{error_messages.empty?}"
|
693
|
+
f.puts ""
|
694
|
+
end
|
695
|
+
|
696
|
+
# エラーメッセージがある場合はダイアログサイズを拡大
|
697
|
+
has_errors = !error_messages.empty?
|
698
|
+
dialog_width = has_errors ? 50 : 35
|
699
|
+
dialog_height = has_errors ? [8 + error_messages.length, 15].min : 6
|
700
|
+
|
701
|
+
# ダイアログの位置を中央に設定
|
702
|
+
x, y = get_screen_center(dialog_width, dialog_height)
|
703
|
+
|
704
|
+
# 成功・失敗に応じた色設定
|
705
|
+
# デバッグ: success_count == total_count かつ has_errors が false の場合のみ成功扱い
|
706
|
+
if success_count == total_count && !has_errors
|
707
|
+
border_color = "\e[32m" # 緑色(成功)
|
708
|
+
title_color = "\e[1;32m" # 太字緑色
|
709
|
+
title = 'Delete Complete'
|
710
|
+
message = "Deleted #{success_count} item(s)"
|
711
|
+
else
|
712
|
+
border_color = "\e[33m" # 黄色(警告)
|
713
|
+
title_color = "\e[1;33m" # 太字黄色
|
714
|
+
title = 'Delete Result'
|
715
|
+
if success_count == total_count && has_errors
|
716
|
+
# 全て削除成功したがエラーメッセージがある場合(本来ここに入らないはず)
|
717
|
+
message = "#{success_count} deleted (with error info)"
|
718
|
+
else
|
719
|
+
failed_count = total_count - success_count
|
720
|
+
message = "#{success_count} deleted, #{failed_count} failed"
|
721
|
+
end
|
722
|
+
end
|
723
|
+
|
724
|
+
# コンテンツの準備
|
725
|
+
content_lines = ['', message]
|
726
|
+
|
727
|
+
# デバッグ情報を追加(開発中のみ)
|
728
|
+
content_lines << ""
|
729
|
+
content_lines << "DEBUG: success=#{success_count}, total=#{total_count}, errors=#{error_messages.length}"
|
730
|
+
|
731
|
+
# エラーメッセージがある場合は追加
|
732
|
+
if has_errors
|
733
|
+
content_lines << ''
|
734
|
+
content_lines << 'Error details:'
|
735
|
+
error_messages.each { |error| content_lines << " #{error}" }
|
736
|
+
end
|
737
|
+
|
738
|
+
content_lines << ''
|
739
|
+
content_lines << 'Press any key to continue...'
|
740
|
+
|
741
|
+
# ダイアログの描画
|
742
|
+
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
743
|
+
border_color: border_color,
|
744
|
+
title_color: title_color,
|
745
|
+
content_color: "\e[37m"
|
746
|
+
})
|
747
|
+
|
748
|
+
# キー入力待機
|
749
|
+
STDIN.getch
|
750
|
+
|
751
|
+
# ダイアログをクリア
|
752
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
753
|
+
@terminal_ui&.refresh_display
|
754
|
+
end
|
755
|
+
|
756
|
+
# フローティングウィンドウの基盤メソッド
|
757
|
+
def draw_floating_window(x, y, width, height, title, content_lines, options = {})
|
758
|
+
# デフォルトオプション
|
759
|
+
border_color = options[:border_color] || "\e[37m" # 白色
|
760
|
+
title_color = options[:title_color] || "\e[1;33m" # 黄色(太字)
|
761
|
+
content_color = options[:content_color] || "\e[37m" # 白色
|
762
|
+
reset_color = "\e[0m"
|
763
|
+
|
764
|
+
# ウィンドウの描画
|
765
|
+
# 上辺
|
766
|
+
print "\e[#{y};#{x}H#{border_color}┌#{'─' * (width - 2)}┐#{reset_color}"
|
767
|
+
|
768
|
+
# タイトル行
|
769
|
+
if title
|
770
|
+
title_width = display_width(title)
|
771
|
+
title_padding = (width - 2 - title_width) / 2
|
772
|
+
padded_title = ' ' * title_padding + title
|
773
|
+
title_line = pad_string_to_width(padded_title, width - 2)
|
774
|
+
print "\e[#{y + 1};#{x}H#{border_color}│#{title_color}#{title_line}#{border_color}│#{reset_color}"
|
775
|
+
|
776
|
+
# タイトル区切り線
|
777
|
+
print "\e[#{y + 2};#{x}H#{border_color}├#{'─' * (width - 2)}┤#{reset_color}"
|
778
|
+
content_start_y = y + 3
|
779
|
+
else
|
780
|
+
content_start_y = y + 1
|
781
|
+
end
|
782
|
+
|
783
|
+
# コンテンツ行
|
784
|
+
content_height = title ? height - 4 : height - 2
|
785
|
+
content_lines.each_with_index do |line, index|
|
786
|
+
break if index >= content_height
|
787
|
+
|
788
|
+
line_y = content_start_y + index
|
789
|
+
line_content = pad_string_to_width(line, width - 2) # 正確な幅でパディング
|
790
|
+
print "\e[#{line_y};#{x}H#{border_color}│#{content_color}#{line_content}#{border_color}│#{reset_color}"
|
791
|
+
end
|
792
|
+
|
793
|
+
# 空行を埋める
|
794
|
+
remaining_lines = content_height - content_lines.length
|
795
|
+
remaining_lines.times do |i|
|
796
|
+
line_y = content_start_y + content_lines.length + i
|
797
|
+
empty_line = ' ' * (width - 2)
|
798
|
+
print "\e[#{line_y};#{x}H#{border_color}│#{empty_line}│#{reset_color}"
|
799
|
+
end
|
800
|
+
|
801
|
+
# 下辺
|
802
|
+
bottom_y = y + height - 1
|
803
|
+
print "\e[#{bottom_y};#{x}H#{border_color}└#{'─' * (width - 2)}┘#{reset_color}"
|
804
|
+
end
|
805
|
+
|
806
|
+
def display_width(str)
|
807
|
+
# 日本語文字の幅を考慮した文字列幅の計算
|
808
|
+
# Unicode East Asian Width プロパティを考慮
|
809
|
+
str.each_char.map do |char|
|
810
|
+
case char
|
811
|
+
when /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\uFF00-\uFFEF]/
|
812
|
+
# 日本語の文字(ひらがな、カタカナ、漢字、全角記号)
|
813
|
+
2
|
814
|
+
when /[\u0020-\u007E]/
|
815
|
+
# ASCII文字
|
816
|
+
1
|
817
|
+
else
|
818
|
+
# その他の文字はバイト数で判断
|
819
|
+
char.bytesize > 1 ? 2 : 1
|
820
|
+
end
|
821
|
+
end.sum
|
822
|
+
end
|
823
|
+
|
824
|
+
def pad_string_to_width(str, target_width)
|
825
|
+
# 文字列を指定した表示幅になるようにパディング
|
826
|
+
current_width = display_width(str)
|
827
|
+
if current_width >= target_width
|
828
|
+
# 文字列が長すぎる場合は切り詰め
|
829
|
+
truncate_to_width(str, target_width)
|
830
|
+
else
|
831
|
+
# 不足分をスペースで埋める
|
832
|
+
str + ' ' * (target_width - current_width)
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
def truncate_to_width(str, max_width)
|
837
|
+
# 指定した表示幅に収まるように文字列を切り詰め
|
838
|
+
result = ''
|
839
|
+
current_width = 0
|
840
|
+
|
841
|
+
str.each_char do |char|
|
842
|
+
char_width = display_width(char)
|
843
|
+
break if current_width + char_width > max_width
|
844
|
+
|
845
|
+
result += char
|
846
|
+
current_width += char_width
|
847
|
+
end
|
848
|
+
|
849
|
+
result
|
850
|
+
end
|
851
|
+
|
852
|
+
def get_screen_center(content_width, content_height)
|
853
|
+
# ターミナルのサイズを取得
|
854
|
+
console = IO.console
|
855
|
+
if console
|
856
|
+
screen_width, screen_height = console.winsize.reverse
|
857
|
+
else
|
858
|
+
screen_width = 80
|
859
|
+
screen_height = 24
|
860
|
+
end
|
861
|
+
|
862
|
+
# 中央位置を計算
|
863
|
+
x = [(screen_width - content_width) / 2, 1].max
|
864
|
+
y = [(screen_height - content_height) / 2, 1].max
|
865
|
+
|
866
|
+
[x, y]
|
867
|
+
end
|
868
|
+
|
869
|
+
def clear_floating_window_area(x, y, width, height)
|
870
|
+
# フローティングウィンドウの領域をクリア
|
871
|
+
height.times do |row|
|
872
|
+
print "\e[#{y + row};#{x}H#{' ' * width}"
|
873
|
+
end
|
874
|
+
end
|
438
875
|
end
|
439
|
-
end
|
876
|
+
end
|