beniya 0.3.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 +16 -38
- data/README_EN.md +17 -38
- data/lib/beniya/file_opener.rb +3 -3
- data/lib/beniya/keybind_handler.rb +376 -86
- data/lib/beniya/terminal_ui.rb +7 -7
- 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,7 +10,7 @@ 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
16
|
@selected_items = []
|
@@ -42,9 +42,7 @@ module Beniya
|
|
42
42
|
return false unless @directory_listing
|
43
43
|
|
44
44
|
# フィルターモード中は他のキーバインドを無効化
|
45
|
-
if @filter_mode
|
46
|
-
return handle_filter_input(key)
|
47
|
-
end
|
45
|
+
return handle_filter_input(key) if @filter_mode
|
48
46
|
|
49
47
|
case key
|
50
48
|
when 'j'
|
@@ -53,7 +51,7 @@ module Beniya
|
|
53
51
|
move_up
|
54
52
|
when 'h'
|
55
53
|
navigate_parent
|
56
|
-
when 'l', "\r", "\n"
|
54
|
+
when 'l', "\r", "\n" # l, Enter
|
57
55
|
navigate_enter
|
58
56
|
when 'g'
|
59
57
|
move_to_top
|
@@ -74,9 +72,9 @@ module Beniya
|
|
74
72
|
# 新規フィルターモード開始
|
75
73
|
start_filter_mode
|
76
74
|
end
|
77
|
-
when ' '
|
75
|
+
when ' ' # Space - toggle selection
|
78
76
|
toggle_selection
|
79
|
-
when "\e"
|
77
|
+
when "\e" # ESC
|
80
78
|
if !@filter_query.empty?
|
81
79
|
# フィルタが設定されている場合はクリア
|
82
80
|
clear_filter_mode
|
@@ -103,7 +101,7 @@ module Beniya
|
|
103
101
|
when 'x' # x - delete selected files
|
104
102
|
delete_selected_files
|
105
103
|
else
|
106
|
-
false
|
104
|
+
false # #{ConfigLoader.message('keybind.invalid_key')}
|
107
105
|
end
|
108
106
|
end
|
109
107
|
|
@@ -121,10 +119,6 @@ module Beniya
|
|
121
119
|
@filter_mode || !@filter_query.empty?
|
122
120
|
end
|
123
121
|
|
124
|
-
def filter_query
|
125
|
-
@filter_query
|
126
|
-
end
|
127
|
-
|
128
122
|
def get_active_entries
|
129
123
|
if @filter_mode || !@filter_query.empty?
|
130
124
|
@filtered_entries.empty? ? [] : @filtered_entries
|
@@ -161,7 +155,7 @@ module Beniya
|
|
161
155
|
entry = current_entry
|
162
156
|
return false unless entry
|
163
157
|
|
164
|
-
if entry[:type] ==
|
158
|
+
if entry[:type] == 'directory'
|
165
159
|
result = @directory_listing.navigate_to(entry[:name])
|
166
160
|
if result
|
167
161
|
@current_index = 0 # select first entry in new directory
|
@@ -186,7 +180,7 @@ module Beniya
|
|
186
180
|
def refresh
|
187
181
|
# ウィンドウサイズを更新して画面を再描画
|
188
182
|
@terminal_ui&.refresh_display
|
189
|
-
|
183
|
+
|
190
184
|
@directory_listing.refresh
|
191
185
|
if @filter_mode || !@filter_query.empty?
|
192
186
|
# Re-apply filter with new directory contents
|
@@ -203,8 +197,8 @@ module Beniya
|
|
203
197
|
def open_current_file
|
204
198
|
entry = current_entry
|
205
199
|
return false unless entry
|
206
|
-
|
207
|
-
if entry[:type] ==
|
200
|
+
|
201
|
+
if entry[:type] == 'file'
|
208
202
|
@file_opener.open_file(entry[:path])
|
209
203
|
true
|
210
204
|
else
|
@@ -219,78 +213,76 @@ module Beniya
|
|
219
213
|
end
|
220
214
|
|
221
215
|
def exit_request
|
222
|
-
true
|
216
|
+
true # request exit
|
223
217
|
end
|
224
218
|
|
225
219
|
def fzf_search
|
226
220
|
return false unless fzf_available?
|
227
|
-
|
221
|
+
|
228
222
|
current_path = @directory_listing&.current_path || Dir.pwd
|
229
|
-
|
223
|
+
|
230
224
|
# fzfでファイル検索を実行
|
231
225
|
selected_file = `cd "#{current_path}" && find . -type f | fzf --preview 'cat {}'`.strip
|
232
|
-
|
226
|
+
|
233
227
|
# ファイルが選択された場合、そのファイルを開く
|
234
228
|
if !selected_file.empty? && File.exist?(File.join(current_path, selected_file))
|
235
229
|
full_path = File.expand_path(selected_file, current_path)
|
236
230
|
@file_opener.open_file(full_path)
|
237
231
|
end
|
238
|
-
|
232
|
+
|
239
233
|
true
|
240
234
|
end
|
241
235
|
|
242
236
|
def fzf_available?
|
243
|
-
system(
|
237
|
+
system('which fzf > /dev/null 2>&1')
|
244
238
|
end
|
245
239
|
|
246
240
|
def rga_search
|
247
241
|
return false unless rga_available?
|
248
|
-
|
242
|
+
|
249
243
|
current_path = @directory_listing&.current_path || Dir.pwd
|
250
|
-
|
244
|
+
|
251
245
|
# input search keyword
|
252
246
|
print ConfigLoader.message('keybind.search_text')
|
253
247
|
search_query = STDIN.gets.chomp
|
254
248
|
return false if search_query.empty?
|
255
|
-
|
249
|
+
|
256
250
|
# execute rga file content search
|
257
251
|
search_results = `cd "#{current_path}" && rga --line-number --with-filename "#{search_query}" . 2>/dev/null`
|
258
|
-
|
252
|
+
|
259
253
|
if search_results.empty?
|
260
254
|
puts "\n#{ConfigLoader.message('keybind.no_matches')}"
|
261
255
|
print ConfigLoader.message('keybind.press_any_key')
|
262
256
|
STDIN.getch
|
263
257
|
return true
|
264
258
|
end
|
265
|
-
|
259
|
+
|
266
260
|
# pass results to fzf for selection
|
267
|
-
selected_result = IO.popen(
|
261
|
+
selected_result = IO.popen('fzf', 'r+') do |fzf|
|
268
262
|
fzf.write(search_results)
|
269
263
|
fzf.close_write
|
270
264
|
fzf.read.strip
|
271
265
|
end
|
272
|
-
|
266
|
+
|
273
267
|
# extract file path and line number from selected result
|
274
268
|
if !selected_result.empty? && selected_result.match(/^(.+?):(\d+):/)
|
275
|
-
file_path =
|
276
|
-
line_number =
|
269
|
+
file_path = ::Regexp.last_match(1)
|
270
|
+
line_number = ::Regexp.last_match(2).to_i
|
277
271
|
full_path = File.expand_path(file_path, current_path)
|
278
|
-
|
279
|
-
if File.exist?(full_path)
|
280
|
-
@file_opener.open_file_with_line(full_path, line_number)
|
281
|
-
end
|
272
|
+
|
273
|
+
@file_opener.open_file_with_line(full_path, line_number) if File.exist?(full_path)
|
282
274
|
end
|
283
|
-
|
275
|
+
|
284
276
|
true
|
285
277
|
end
|
286
278
|
|
287
279
|
def rga_available?
|
288
|
-
system(
|
280
|
+
system('which rga > /dev/null 2>&1')
|
289
281
|
end
|
290
282
|
|
291
283
|
def start_filter_mode
|
292
284
|
@filter_mode = true
|
293
|
-
@filter_query =
|
285
|
+
@filter_query = ''
|
294
286
|
@original_entries = @directory_listing.list_entries.dup
|
295
287
|
@filtered_entries = @original_entries.dup
|
296
288
|
@current_index = 0
|
@@ -299,11 +291,11 @@ module Beniya
|
|
299
291
|
|
300
292
|
def handle_filter_input(key)
|
301
293
|
case key
|
302
|
-
when "\e"
|
294
|
+
when "\e" # ESC - フィルタをクリアして通常モードに戻る
|
303
295
|
clear_filter_mode
|
304
|
-
when "\r", "\n"
|
296
|
+
when "\r", "\n" # Enter - フィルタを維持して通常モードに戻る
|
305
297
|
exit_filter_mode_keep_filter
|
306
|
-
when "\u007f", "\b"
|
298
|
+
when "\u007f", "\b" # Backspace
|
307
299
|
if @filter_query.length > 0
|
308
300
|
@filter_query = @filter_query[0...-1]
|
309
301
|
apply_filter
|
@@ -312,10 +304,10 @@ module Beniya
|
|
312
304
|
end
|
313
305
|
else
|
314
306
|
# printable characters (英数字、記号、日本語文字など)
|
315
|
-
if key.length == 1 && key.ord >= 32 && key.ord < 127
|
307
|
+
if key.length == 1 && key.ord >= 32 && key.ord < 127 # ASCII printable
|
316
308
|
@filter_query += key
|
317
309
|
apply_filter
|
318
|
-
elsif key.bytesize > 1
|
310
|
+
elsif key.bytesize > 1 # Multi-byte characters (Japanese, etc.)
|
319
311
|
@filter_query += key
|
320
312
|
apply_filter
|
321
313
|
end
|
@@ -345,7 +337,7 @@ module Beniya
|
|
345
337
|
def clear_filter_mode
|
346
338
|
# フィルタをクリアして通常モードに戻る
|
347
339
|
@filter_mode = false
|
348
|
-
@filter_query =
|
340
|
+
@filter_query = ''
|
349
341
|
@filtered_entries = []
|
350
342
|
@original_entries = []
|
351
343
|
@current_index = 0
|
@@ -358,12 +350,12 @@ module Beniya
|
|
358
350
|
|
359
351
|
def create_file
|
360
352
|
current_path = @directory_listing&.current_path || Dir.pwd
|
361
|
-
|
353
|
+
|
362
354
|
# ファイル名の入力を求める
|
363
355
|
print ConfigLoader.message('keybind.input_filename')
|
364
356
|
filename = STDIN.gets.chomp
|
365
357
|
return false if filename.empty?
|
366
|
-
|
358
|
+
|
367
359
|
# 不正なファイル名のチェック
|
368
360
|
if filename.include?('/') || filename.include?('\\')
|
369
361
|
puts "\n#{ConfigLoader.message('keybind.invalid_filename')}"
|
@@ -371,9 +363,9 @@ module Beniya
|
|
371
363
|
STDIN.getch
|
372
364
|
return false
|
373
365
|
end
|
374
|
-
|
366
|
+
|
375
367
|
file_path = File.join(current_path, filename)
|
376
|
-
|
368
|
+
|
377
369
|
# ファイルが既に存在する場合の確認
|
378
370
|
if File.exist?(file_path)
|
379
371
|
puts "\n#{ConfigLoader.message('keybind.file_exists')}"
|
@@ -381,24 +373,24 @@ module Beniya
|
|
381
373
|
STDIN.getch
|
382
374
|
return false
|
383
375
|
end
|
384
|
-
|
376
|
+
|
385
377
|
begin
|
386
378
|
# ファイルを作成
|
387
379
|
File.write(file_path, '')
|
388
|
-
|
380
|
+
|
389
381
|
# ディレクトリ表示を更新
|
390
382
|
@directory_listing.refresh
|
391
|
-
|
383
|
+
|
392
384
|
# 作成したファイルを選択状態にする
|
393
385
|
entries = @directory_listing.list_entries
|
394
386
|
new_file_index = entries.find_index { |entry| entry[:name] == filename }
|
395
387
|
@current_index = new_file_index if new_file_index
|
396
|
-
|
388
|
+
|
397
389
|
puts "\n#{ConfigLoader.message('keybind.file_created')}: #{filename}"
|
398
390
|
print ConfigLoader.message('keybind.press_any_key')
|
399
391
|
STDIN.getch
|
400
392
|
true
|
401
|
-
rescue => e
|
393
|
+
rescue StandardError => e
|
402
394
|
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
403
395
|
print ConfigLoader.message('keybind.press_any_key')
|
404
396
|
STDIN.getch
|
@@ -408,12 +400,12 @@ module Beniya
|
|
408
400
|
|
409
401
|
def create_directory
|
410
402
|
current_path = @directory_listing&.current_path || Dir.pwd
|
411
|
-
|
403
|
+
|
412
404
|
# ディレクトリ名の入力を求める
|
413
405
|
print ConfigLoader.message('keybind.input_dirname')
|
414
406
|
dirname = STDIN.gets.chomp
|
415
407
|
return false if dirname.empty?
|
416
|
-
|
408
|
+
|
417
409
|
# 不正なディレクトリ名のチェック
|
418
410
|
if dirname.include?('/') || dirname.include?('\\')
|
419
411
|
puts "\n#{ConfigLoader.message('keybind.invalid_dirname')}"
|
@@ -421,9 +413,9 @@ module Beniya
|
|
421
413
|
STDIN.getch
|
422
414
|
return false
|
423
415
|
end
|
424
|
-
|
416
|
+
|
425
417
|
dir_path = File.join(current_path, dirname)
|
426
|
-
|
418
|
+
|
427
419
|
# ディレクトリが既に存在する場合の確認
|
428
420
|
if File.exist?(dir_path)
|
429
421
|
puts "\n#{ConfigLoader.message('keybind.directory_exists')}"
|
@@ -431,24 +423,24 @@ module Beniya
|
|
431
423
|
STDIN.getch
|
432
424
|
return false
|
433
425
|
end
|
434
|
-
|
426
|
+
|
435
427
|
begin
|
436
428
|
# ディレクトリを作成
|
437
429
|
Dir.mkdir(dir_path)
|
438
|
-
|
430
|
+
|
439
431
|
# ディレクトリ表示を更新
|
440
432
|
@directory_listing.refresh
|
441
|
-
|
433
|
+
|
442
434
|
# 作成したディレクトリを選択状態にする
|
443
435
|
entries = @directory_listing.list_entries
|
444
436
|
new_dir_index = entries.find_index { |entry| entry[:name] == dirname }
|
445
437
|
@current_index = new_dir_index if new_dir_index
|
446
|
-
|
438
|
+
|
447
439
|
puts "\n#{ConfigLoader.message('keybind.directory_created')}: #{dirname}"
|
448
440
|
print ConfigLoader.message('keybind.press_any_key')
|
449
441
|
STDIN.getch
|
450
442
|
true
|
451
|
-
rescue => e
|
443
|
+
rescue StandardError => e
|
452
444
|
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
453
445
|
print ConfigLoader.message('keybind.press_any_key')
|
454
446
|
STDIN.getch
|
@@ -471,7 +463,7 @@ module Beniya
|
|
471
463
|
def move_selected_to_base
|
472
464
|
return false if @selected_items.empty? || @base_directory.nil?
|
473
465
|
|
474
|
-
if show_confirmation_dialog(
|
466
|
+
if show_confirmation_dialog('Move', @selected_items.length)
|
475
467
|
perform_file_operation(:move, @selected_items, @base_directory)
|
476
468
|
else
|
477
469
|
false
|
@@ -481,7 +473,7 @@ module Beniya
|
|
481
473
|
def copy_selected_to_base
|
482
474
|
return false if @selected_items.empty? || @base_directory.nil?
|
483
475
|
|
484
|
-
if show_confirmation_dialog(
|
476
|
+
if show_confirmation_dialog('Copy', @selected_items.length)
|
485
477
|
perform_file_operation(:copy, @selected_items, @base_directory)
|
486
478
|
else
|
487
479
|
false
|
@@ -489,9 +481,9 @@ module Beniya
|
|
489
481
|
end
|
490
482
|
|
491
483
|
def show_confirmation_dialog(operation, count)
|
492
|
-
print "\n#{
|
484
|
+
print "\n#{operation} #{count} item(s)? (y/n): "
|
493
485
|
response = STDIN.gets.chomp.downcase
|
494
|
-
|
486
|
+
%w[y yes].include?(response)
|
495
487
|
end
|
496
488
|
|
497
489
|
def perform_file_operation(operation, items, destination)
|
@@ -506,13 +498,13 @@ module Beniya
|
|
506
498
|
case operation
|
507
499
|
when :move
|
508
500
|
if File.exist?(dest_path)
|
509
|
-
puts "\n#{item_name}
|
501
|
+
puts "\n#{item_name} already exists in destination. Skipping."
|
510
502
|
next
|
511
503
|
end
|
512
504
|
FileUtils.mv(source_path, dest_path)
|
513
505
|
when :copy
|
514
506
|
if File.exist?(dest_path)
|
515
|
-
puts "\n#{item_name}
|
507
|
+
puts "\n#{item_name} already exists in destination. Skipping."
|
516
508
|
next
|
517
509
|
end
|
518
510
|
if File.directory?(source_path)
|
@@ -522,17 +514,17 @@ module Beniya
|
|
522
514
|
end
|
523
515
|
end
|
524
516
|
success_count += 1
|
525
|
-
rescue => e
|
526
|
-
puts "\
|
517
|
+
rescue StandardError => e
|
518
|
+
puts "\nFailed to #{operation == :move ? 'move' : 'copy'} #{item_name}: #{e.message}"
|
527
519
|
end
|
528
520
|
end
|
529
521
|
|
530
522
|
# 操作完了後の処理
|
531
523
|
@selected_items.clear
|
532
524
|
@directory_listing.refresh if @directory_listing
|
533
|
-
|
534
|
-
puts "\n#{
|
535
|
-
print
|
525
|
+
|
526
|
+
puts "\n#{operation == :move ? 'Moved' : 'Copied'} #{success_count} item(s)."
|
527
|
+
print 'Press any key to continue...'
|
536
528
|
STDIN.getch
|
537
529
|
true
|
538
530
|
end
|
@@ -548,39 +540,337 @@ module Beniya
|
|
548
540
|
end
|
549
541
|
|
550
542
|
def show_delete_confirmation(count)
|
551
|
-
|
552
|
-
|
553
|
-
|
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
|
554
598
|
end
|
555
599
|
|
556
600
|
def perform_delete_operation(items)
|
557
601
|
success_count = 0
|
602
|
+
error_messages = []
|
558
603
|
current_path = @directory_listing&.current_path || Dir.pwd
|
604
|
+
debug_log = []
|
559
605
|
|
560
606
|
items.each do |item_name|
|
561
607
|
item_path = File.join(current_path, item_name)
|
608
|
+
debug_log << "Processing: #{item_name}"
|
562
609
|
|
563
610
|
begin
|
564
|
-
|
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
|
565
623
|
FileUtils.rm_rf(item_path)
|
624
|
+
debug_log << ' FileUtils.rm_rf executed'
|
566
625
|
else
|
567
626
|
FileUtils.rm(item_path)
|
627
|
+
debug_log << ' FileUtils.rm executed'
|
568
628
|
end
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
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}"
|
573
645
|
end
|
574
646
|
end
|
575
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
|
+
|
576
678
|
# 削除完了後の処理
|
577
679
|
@selected_items.clear
|
578
680
|
@directory_listing.refresh if @directory_listing
|
579
|
-
|
580
|
-
puts "\n#{success_count}個のアイテムを削除しました。"
|
581
|
-
print "何かキーを押してください..."
|
582
|
-
STDIN.getch
|
681
|
+
|
583
682
|
true
|
584
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
|
585
875
|
end
|
586
876
|
end
|