beniya 0.3.0 → 0.5.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 +124 -0
- data/CHANGELOG_v0.4.0.md +146 -0
- data/CHANGELOG_v0.5.0.md +26 -0
- data/README.md +44 -38
- data/README_EN.md +17 -38
- data/lib/beniya/bookmark.rb +115 -0
- data/lib/beniya/config.rb +4 -4
- data/lib/beniya/directory_listing.rb +14 -0
- data/lib/beniya/file_opener.rb +3 -3
- data/lib/beniya/keybind_handler.rb +547 -84
- data/lib/beniya/terminal_ui.rb +8 -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 +9 -2
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'bookmark'
|
4
|
+
|
3
5
|
module Beniya
|
4
6
|
class KeybindHandler
|
5
|
-
attr_reader :current_index
|
7
|
+
attr_reader :current_index, :filter_query
|
6
8
|
|
7
9
|
def initialize
|
8
10
|
@current_index = 0
|
@@ -10,11 +12,12 @@ module Beniya
|
|
10
12
|
@terminal_ui = nil
|
11
13
|
@file_opener = FileOpener.new
|
12
14
|
@filter_mode = false
|
13
|
-
@filter_query =
|
15
|
+
@filter_query = ''
|
14
16
|
@filtered_entries = []
|
15
17
|
@original_entries = []
|
16
18
|
@selected_items = []
|
17
19
|
@base_directory = nil
|
20
|
+
@bookmark = Bookmark.new
|
18
21
|
end
|
19
22
|
|
20
23
|
def set_directory_listing(directory_listing)
|
@@ -42,9 +45,7 @@ module Beniya
|
|
42
45
|
return false unless @directory_listing
|
43
46
|
|
44
47
|
# フィルターモード中は他のキーバインドを無効化
|
45
|
-
if @filter_mode
|
46
|
-
return handle_filter_input(key)
|
47
|
-
end
|
48
|
+
return handle_filter_input(key) if @filter_mode
|
48
49
|
|
49
50
|
case key
|
50
51
|
when 'j'
|
@@ -53,7 +54,7 @@ module Beniya
|
|
53
54
|
move_up
|
54
55
|
when 'h'
|
55
56
|
navigate_parent
|
56
|
-
when 'l', "\r", "\n"
|
57
|
+
when 'l', "\r", "\n" # l, Enter
|
57
58
|
navigate_enter
|
58
59
|
when 'g'
|
59
60
|
move_to_top
|
@@ -74,9 +75,9 @@ module Beniya
|
|
74
75
|
# 新規フィルターモード開始
|
75
76
|
start_filter_mode
|
76
77
|
end
|
77
|
-
when ' '
|
78
|
+
when ' ' # Space - toggle selection
|
78
79
|
toggle_selection
|
79
|
-
when "\e"
|
80
|
+
when "\e" # ESC
|
80
81
|
if !@filter_query.empty?
|
81
82
|
# フィルタが設定されている場合はクリア
|
82
83
|
clear_filter_mode
|
@@ -102,8 +103,12 @@ module Beniya
|
|
102
103
|
copy_selected_to_base
|
103
104
|
when 'x' # x - delete selected files
|
104
105
|
delete_selected_files
|
106
|
+
when 'b' # b - bookmark operations
|
107
|
+
show_bookmark_menu
|
108
|
+
when '1', '2', '3', '4', '5', '6', '7', '8', '9' # number keys - go to bookmark
|
109
|
+
goto_bookmark(key.to_i)
|
105
110
|
else
|
106
|
-
false
|
111
|
+
false # #{ConfigLoader.message('keybind.invalid_key')}
|
107
112
|
end
|
108
113
|
end
|
109
114
|
|
@@ -121,10 +126,6 @@ module Beniya
|
|
121
126
|
@filter_mode || !@filter_query.empty?
|
122
127
|
end
|
123
128
|
|
124
|
-
def filter_query
|
125
|
-
@filter_query
|
126
|
-
end
|
127
|
-
|
128
129
|
def get_active_entries
|
129
130
|
if @filter_mode || !@filter_query.empty?
|
130
131
|
@filtered_entries.empty? ? [] : @filtered_entries
|
@@ -161,7 +162,7 @@ module Beniya
|
|
161
162
|
entry = current_entry
|
162
163
|
return false unless entry
|
163
164
|
|
164
|
-
if entry[:type] ==
|
165
|
+
if entry[:type] == 'directory'
|
165
166
|
result = @directory_listing.navigate_to(entry[:name])
|
166
167
|
if result
|
167
168
|
@current_index = 0 # select first entry in new directory
|
@@ -186,7 +187,7 @@ module Beniya
|
|
186
187
|
def refresh
|
187
188
|
# ウィンドウサイズを更新して画面を再描画
|
188
189
|
@terminal_ui&.refresh_display
|
189
|
-
|
190
|
+
|
190
191
|
@directory_listing.refresh
|
191
192
|
if @filter_mode || !@filter_query.empty?
|
192
193
|
# Re-apply filter with new directory contents
|
@@ -203,8 +204,8 @@ module Beniya
|
|
203
204
|
def open_current_file
|
204
205
|
entry = current_entry
|
205
206
|
return false unless entry
|
206
|
-
|
207
|
-
if entry[:type] ==
|
207
|
+
|
208
|
+
if entry[:type] == 'file'
|
208
209
|
@file_opener.open_file(entry[:path])
|
209
210
|
true
|
210
211
|
else
|
@@ -219,78 +220,76 @@ module Beniya
|
|
219
220
|
end
|
220
221
|
|
221
222
|
def exit_request
|
222
|
-
true
|
223
|
+
true # request exit
|
223
224
|
end
|
224
225
|
|
225
226
|
def fzf_search
|
226
227
|
return false unless fzf_available?
|
227
|
-
|
228
|
+
|
228
229
|
current_path = @directory_listing&.current_path || Dir.pwd
|
229
|
-
|
230
|
+
|
230
231
|
# fzfでファイル検索を実行
|
231
232
|
selected_file = `cd "#{current_path}" && find . -type f | fzf --preview 'cat {}'`.strip
|
232
|
-
|
233
|
+
|
233
234
|
# ファイルが選択された場合、そのファイルを開く
|
234
235
|
if !selected_file.empty? && File.exist?(File.join(current_path, selected_file))
|
235
236
|
full_path = File.expand_path(selected_file, current_path)
|
236
237
|
@file_opener.open_file(full_path)
|
237
238
|
end
|
238
|
-
|
239
|
+
|
239
240
|
true
|
240
241
|
end
|
241
242
|
|
242
243
|
def fzf_available?
|
243
|
-
system(
|
244
|
+
system('which fzf > /dev/null 2>&1')
|
244
245
|
end
|
245
246
|
|
246
247
|
def rga_search
|
247
248
|
return false unless rga_available?
|
248
|
-
|
249
|
+
|
249
250
|
current_path = @directory_listing&.current_path || Dir.pwd
|
250
|
-
|
251
|
+
|
251
252
|
# input search keyword
|
252
253
|
print ConfigLoader.message('keybind.search_text')
|
253
254
|
search_query = STDIN.gets.chomp
|
254
255
|
return false if search_query.empty?
|
255
|
-
|
256
|
+
|
256
257
|
# execute rga file content search
|
257
258
|
search_results = `cd "#{current_path}" && rga --line-number --with-filename "#{search_query}" . 2>/dev/null`
|
258
|
-
|
259
|
+
|
259
260
|
if search_results.empty?
|
260
261
|
puts "\n#{ConfigLoader.message('keybind.no_matches')}"
|
261
262
|
print ConfigLoader.message('keybind.press_any_key')
|
262
263
|
STDIN.getch
|
263
264
|
return true
|
264
265
|
end
|
265
|
-
|
266
|
+
|
266
267
|
# pass results to fzf for selection
|
267
|
-
selected_result = IO.popen(
|
268
|
+
selected_result = IO.popen('fzf', 'r+') do |fzf|
|
268
269
|
fzf.write(search_results)
|
269
270
|
fzf.close_write
|
270
271
|
fzf.read.strip
|
271
272
|
end
|
272
|
-
|
273
|
+
|
273
274
|
# extract file path and line number from selected result
|
274
275
|
if !selected_result.empty? && selected_result.match(/^(.+?):(\d+):/)
|
275
|
-
file_path =
|
276
|
-
line_number =
|
276
|
+
file_path = ::Regexp.last_match(1)
|
277
|
+
line_number = ::Regexp.last_match(2).to_i
|
277
278
|
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
|
279
|
+
|
280
|
+
@file_opener.open_file_with_line(full_path, line_number) if File.exist?(full_path)
|
282
281
|
end
|
283
|
-
|
282
|
+
|
284
283
|
true
|
285
284
|
end
|
286
285
|
|
287
286
|
def rga_available?
|
288
|
-
system(
|
287
|
+
system('which rga > /dev/null 2>&1')
|
289
288
|
end
|
290
289
|
|
291
290
|
def start_filter_mode
|
292
291
|
@filter_mode = true
|
293
|
-
@filter_query =
|
292
|
+
@filter_query = ''
|
294
293
|
@original_entries = @directory_listing.list_entries.dup
|
295
294
|
@filtered_entries = @original_entries.dup
|
296
295
|
@current_index = 0
|
@@ -299,11 +298,11 @@ module Beniya
|
|
299
298
|
|
300
299
|
def handle_filter_input(key)
|
301
300
|
case key
|
302
|
-
when "\e"
|
301
|
+
when "\e" # ESC - フィルタをクリアして通常モードに戻る
|
303
302
|
clear_filter_mode
|
304
|
-
when "\r", "\n"
|
303
|
+
when "\r", "\n" # Enter - フィルタを維持して通常モードに戻る
|
305
304
|
exit_filter_mode_keep_filter
|
306
|
-
when "\u007f", "\b"
|
305
|
+
when "\u007f", "\b" # Backspace
|
307
306
|
if @filter_query.length > 0
|
308
307
|
@filter_query = @filter_query[0...-1]
|
309
308
|
apply_filter
|
@@ -312,10 +311,10 @@ module Beniya
|
|
312
311
|
end
|
313
312
|
else
|
314
313
|
# printable characters (英数字、記号、日本語文字など)
|
315
|
-
if key.length == 1 && key.ord >= 32 && key.ord < 127
|
314
|
+
if key.length == 1 && key.ord >= 32 && key.ord < 127 # ASCII printable
|
316
315
|
@filter_query += key
|
317
316
|
apply_filter
|
318
|
-
elsif key.bytesize > 1
|
317
|
+
elsif key.bytesize > 1 # Multi-byte characters (Japanese, etc.)
|
319
318
|
@filter_query += key
|
320
319
|
apply_filter
|
321
320
|
end
|
@@ -345,7 +344,7 @@ module Beniya
|
|
345
344
|
def clear_filter_mode
|
346
345
|
# フィルタをクリアして通常モードに戻る
|
347
346
|
@filter_mode = false
|
348
|
-
@filter_query =
|
347
|
+
@filter_query = ''
|
349
348
|
@filtered_entries = []
|
350
349
|
@original_entries = []
|
351
350
|
@current_index = 0
|
@@ -358,12 +357,12 @@ module Beniya
|
|
358
357
|
|
359
358
|
def create_file
|
360
359
|
current_path = @directory_listing&.current_path || Dir.pwd
|
361
|
-
|
360
|
+
|
362
361
|
# ファイル名の入力を求める
|
363
362
|
print ConfigLoader.message('keybind.input_filename')
|
364
363
|
filename = STDIN.gets.chomp
|
365
364
|
return false if filename.empty?
|
366
|
-
|
365
|
+
|
367
366
|
# 不正なファイル名のチェック
|
368
367
|
if filename.include?('/') || filename.include?('\\')
|
369
368
|
puts "\n#{ConfigLoader.message('keybind.invalid_filename')}"
|
@@ -371,9 +370,9 @@ module Beniya
|
|
371
370
|
STDIN.getch
|
372
371
|
return false
|
373
372
|
end
|
374
|
-
|
373
|
+
|
375
374
|
file_path = File.join(current_path, filename)
|
376
|
-
|
375
|
+
|
377
376
|
# ファイルが既に存在する場合の確認
|
378
377
|
if File.exist?(file_path)
|
379
378
|
puts "\n#{ConfigLoader.message('keybind.file_exists')}"
|
@@ -381,24 +380,24 @@ module Beniya
|
|
381
380
|
STDIN.getch
|
382
381
|
return false
|
383
382
|
end
|
384
|
-
|
383
|
+
|
385
384
|
begin
|
386
385
|
# ファイルを作成
|
387
386
|
File.write(file_path, '')
|
388
|
-
|
387
|
+
|
389
388
|
# ディレクトリ表示を更新
|
390
389
|
@directory_listing.refresh
|
391
|
-
|
390
|
+
|
392
391
|
# 作成したファイルを選択状態にする
|
393
392
|
entries = @directory_listing.list_entries
|
394
393
|
new_file_index = entries.find_index { |entry| entry[:name] == filename }
|
395
394
|
@current_index = new_file_index if new_file_index
|
396
|
-
|
395
|
+
|
397
396
|
puts "\n#{ConfigLoader.message('keybind.file_created')}: #{filename}"
|
398
397
|
print ConfigLoader.message('keybind.press_any_key')
|
399
398
|
STDIN.getch
|
400
399
|
true
|
401
|
-
rescue => e
|
400
|
+
rescue StandardError => e
|
402
401
|
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
403
402
|
print ConfigLoader.message('keybind.press_any_key')
|
404
403
|
STDIN.getch
|
@@ -408,12 +407,12 @@ module Beniya
|
|
408
407
|
|
409
408
|
def create_directory
|
410
409
|
current_path = @directory_listing&.current_path || Dir.pwd
|
411
|
-
|
410
|
+
|
412
411
|
# ディレクトリ名の入力を求める
|
413
412
|
print ConfigLoader.message('keybind.input_dirname')
|
414
413
|
dirname = STDIN.gets.chomp
|
415
414
|
return false if dirname.empty?
|
416
|
-
|
415
|
+
|
417
416
|
# 不正なディレクトリ名のチェック
|
418
417
|
if dirname.include?('/') || dirname.include?('\\')
|
419
418
|
puts "\n#{ConfigLoader.message('keybind.invalid_dirname')}"
|
@@ -421,9 +420,9 @@ module Beniya
|
|
421
420
|
STDIN.getch
|
422
421
|
return false
|
423
422
|
end
|
424
|
-
|
423
|
+
|
425
424
|
dir_path = File.join(current_path, dirname)
|
426
|
-
|
425
|
+
|
427
426
|
# ディレクトリが既に存在する場合の確認
|
428
427
|
if File.exist?(dir_path)
|
429
428
|
puts "\n#{ConfigLoader.message('keybind.directory_exists')}"
|
@@ -431,24 +430,24 @@ module Beniya
|
|
431
430
|
STDIN.getch
|
432
431
|
return false
|
433
432
|
end
|
434
|
-
|
433
|
+
|
435
434
|
begin
|
436
435
|
# ディレクトリを作成
|
437
436
|
Dir.mkdir(dir_path)
|
438
|
-
|
437
|
+
|
439
438
|
# ディレクトリ表示を更新
|
440
439
|
@directory_listing.refresh
|
441
|
-
|
440
|
+
|
442
441
|
# 作成したディレクトリを選択状態にする
|
443
442
|
entries = @directory_listing.list_entries
|
444
443
|
new_dir_index = entries.find_index { |entry| entry[:name] == dirname }
|
445
444
|
@current_index = new_dir_index if new_dir_index
|
446
|
-
|
445
|
+
|
447
446
|
puts "\n#{ConfigLoader.message('keybind.directory_created')}: #{dirname}"
|
448
447
|
print ConfigLoader.message('keybind.press_any_key')
|
449
448
|
STDIN.getch
|
450
449
|
true
|
451
|
-
rescue => e
|
450
|
+
rescue StandardError => e
|
452
451
|
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
453
452
|
print ConfigLoader.message('keybind.press_any_key')
|
454
453
|
STDIN.getch
|
@@ -471,7 +470,7 @@ module Beniya
|
|
471
470
|
def move_selected_to_base
|
472
471
|
return false if @selected_items.empty? || @base_directory.nil?
|
473
472
|
|
474
|
-
if show_confirmation_dialog(
|
473
|
+
if show_confirmation_dialog('Move', @selected_items.length)
|
475
474
|
perform_file_operation(:move, @selected_items, @base_directory)
|
476
475
|
else
|
477
476
|
false
|
@@ -481,7 +480,7 @@ module Beniya
|
|
481
480
|
def copy_selected_to_base
|
482
481
|
return false if @selected_items.empty? || @base_directory.nil?
|
483
482
|
|
484
|
-
if show_confirmation_dialog(
|
483
|
+
if show_confirmation_dialog('Copy', @selected_items.length)
|
485
484
|
perform_file_operation(:copy, @selected_items, @base_directory)
|
486
485
|
else
|
487
486
|
false
|
@@ -489,9 +488,9 @@ module Beniya
|
|
489
488
|
end
|
490
489
|
|
491
490
|
def show_confirmation_dialog(operation, count)
|
492
|
-
print "\n#{
|
491
|
+
print "\n#{operation} #{count} item(s)? (y/n): "
|
493
492
|
response = STDIN.gets.chomp.downcase
|
494
|
-
|
493
|
+
%w[y yes].include?(response)
|
495
494
|
end
|
496
495
|
|
497
496
|
def perform_file_operation(operation, items, destination)
|
@@ -506,13 +505,13 @@ module Beniya
|
|
506
505
|
case operation
|
507
506
|
when :move
|
508
507
|
if File.exist?(dest_path)
|
509
|
-
puts "\n#{item_name}
|
508
|
+
puts "\n#{item_name} already exists in destination. Skipping."
|
510
509
|
next
|
511
510
|
end
|
512
511
|
FileUtils.mv(source_path, dest_path)
|
513
512
|
when :copy
|
514
513
|
if File.exist?(dest_path)
|
515
|
-
puts "\n#{item_name}
|
514
|
+
puts "\n#{item_name} already exists in destination. Skipping."
|
516
515
|
next
|
517
516
|
end
|
518
517
|
if File.directory?(source_path)
|
@@ -522,17 +521,17 @@ module Beniya
|
|
522
521
|
end
|
523
522
|
end
|
524
523
|
success_count += 1
|
525
|
-
rescue => e
|
526
|
-
puts "\
|
524
|
+
rescue StandardError => e
|
525
|
+
puts "\nFailed to #{operation == :move ? 'move' : 'copy'} #{item_name}: #{e.message}"
|
527
526
|
end
|
528
527
|
end
|
529
528
|
|
530
529
|
# 操作完了後の処理
|
531
530
|
@selected_items.clear
|
532
531
|
@directory_listing.refresh if @directory_listing
|
533
|
-
|
534
|
-
puts "\n#{
|
535
|
-
print
|
532
|
+
|
533
|
+
puts "\n#{operation == :move ? 'Moved' : 'Copied'} #{success_count} item(s)."
|
534
|
+
print 'Press any key to continue...'
|
536
535
|
STDIN.getch
|
537
536
|
true
|
538
537
|
end
|
@@ -548,39 +547,503 @@ module Beniya
|
|
548
547
|
end
|
549
548
|
|
550
549
|
def show_delete_confirmation(count)
|
551
|
-
|
552
|
-
|
553
|
-
|
550
|
+
show_floating_delete_confirmation(count)
|
551
|
+
end
|
552
|
+
|
553
|
+
def show_floating_delete_confirmation(count)
|
554
|
+
# コンテンツの準備
|
555
|
+
title = 'Delete Confirmation'
|
556
|
+
content_lines = [
|
557
|
+
'',
|
558
|
+
"Delete #{count} item(s)?",
|
559
|
+
'',
|
560
|
+
' [Y]es - Delete',
|
561
|
+
' [N]o - Cancel',
|
562
|
+
''
|
563
|
+
]
|
564
|
+
|
565
|
+
# ダイアログのサイズ設定(コンテンツに合わせて調整)
|
566
|
+
dialog_width = 45
|
567
|
+
# タイトルあり: 上枠1 + タイトル1 + 区切り1 + コンテンツ6 + 下枠1 = 10
|
568
|
+
dialog_height = 4 + content_lines.length
|
569
|
+
|
570
|
+
# ダイアログの位置を中央に設定
|
571
|
+
x, y = get_screen_center(dialog_width, dialog_height)
|
572
|
+
|
573
|
+
# ダイアログの描画
|
574
|
+
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
575
|
+
border_color: "\e[31m", # 赤色(警告)
|
576
|
+
title_color: "\e[1;31m", # 太字赤色
|
577
|
+
content_color: "\e[37m" # 白色
|
578
|
+
})
|
579
|
+
|
580
|
+
# フラッシュしてユーザーの注意を引く
|
581
|
+
print "\a" # ベル音
|
582
|
+
|
583
|
+
# キー入力待機
|
584
|
+
loop do
|
585
|
+
input = STDIN.getch.downcase
|
586
|
+
|
587
|
+
case input
|
588
|
+
when 'y'
|
589
|
+
# ダイアログをクリア
|
590
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
591
|
+
@terminal_ui&.refresh_display # 画面を再描画
|
592
|
+
return true
|
593
|
+
when 'n', "\e", "\x03" # n, ESC, Ctrl+C
|
594
|
+
# ダイアログをクリア
|
595
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
596
|
+
@terminal_ui&.refresh_display # 画面を再描画
|
597
|
+
return false
|
598
|
+
when 'q' # qキーでもキャンセル
|
599
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
600
|
+
@terminal_ui&.refresh_display
|
601
|
+
return false
|
602
|
+
end
|
603
|
+
# 無効なキー入力の場合は再度ループ
|
604
|
+
end
|
554
605
|
end
|
555
606
|
|
556
607
|
def perform_delete_operation(items)
|
557
608
|
success_count = 0
|
609
|
+
error_messages = []
|
558
610
|
current_path = @directory_listing&.current_path || Dir.pwd
|
611
|
+
debug_log = []
|
559
612
|
|
560
613
|
items.each do |item_name|
|
561
614
|
item_path = File.join(current_path, item_name)
|
615
|
+
debug_log << "Processing: #{item_name}"
|
562
616
|
|
563
617
|
begin
|
564
|
-
|
618
|
+
# ファイル/ディレクトリの存在確認
|
619
|
+
unless File.exist?(item_path)
|
620
|
+
error_messages << "#{item_name}: File not found"
|
621
|
+
debug_log << ' Error: File not found'
|
622
|
+
next
|
623
|
+
end
|
624
|
+
|
625
|
+
debug_log << ' Existence check: OK'
|
626
|
+
is_directory = File.directory?(item_path)
|
627
|
+
debug_log << " Type: #{is_directory ? 'Directory' : 'File'}"
|
628
|
+
|
629
|
+
if is_directory
|
565
630
|
FileUtils.rm_rf(item_path)
|
631
|
+
debug_log << ' FileUtils.rm_rf executed'
|
566
632
|
else
|
567
633
|
FileUtils.rm(item_path)
|
634
|
+
debug_log << ' FileUtils.rm executed'
|
568
635
|
end
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
636
|
+
|
637
|
+
# 削除が実際に成功したかを確認
|
638
|
+
sleep(0.01) # 10ms待機してファイルシステムの同期を待つ
|
639
|
+
still_exists = File.exist?(item_path)
|
640
|
+
debug_log << " Post-deletion check: #{still_exists}"
|
641
|
+
|
642
|
+
if still_exists
|
643
|
+
error_messages << "#{item_name}: Deletion failed"
|
644
|
+
debug_log << ' Result: Failed'
|
645
|
+
else
|
646
|
+
success_count += 1
|
647
|
+
debug_log << ' Result: Success'
|
648
|
+
end
|
649
|
+
rescue StandardError => e
|
650
|
+
error_messages << "#{item_name}: #{e.message}"
|
651
|
+
debug_log << " Exception: #{e.message}"
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
# デバッグログをファイルに出力(開発時のみ)
|
656
|
+
if ENV['BENIYA_DEBUG'] == '1'
|
657
|
+
debug_file = File.join(Dir.home, '.beniya_delete_debug.log')
|
658
|
+
File.open(debug_file, 'a') do |f|
|
659
|
+
f.puts "=== Delete Process Debug #{Time.now} ==="
|
660
|
+
f.puts "Target directory: #{current_path}"
|
661
|
+
f.puts "Target items: #{items.inspect}"
|
662
|
+
debug_log.each { |line| f.puts line }
|
663
|
+
f.puts "Final result: #{success_count} successful, #{items.length - success_count} failed"
|
664
|
+
f.puts "Error messages: #{error_messages.inspect}"
|
665
|
+
f.puts ''
|
573
666
|
end
|
574
667
|
end
|
575
668
|
|
669
|
+
|
670
|
+
# デバッグ用:削除結果の値をログファイルに出力
|
671
|
+
result_debug_file = File.join(Dir.home, '.beniya_result_debug.log')
|
672
|
+
File.open(result_debug_file, 'a') do |f|
|
673
|
+
f.puts "=== Delete Result Debug #{Time.now} ==="
|
674
|
+
f.puts "success_count: #{success_count}"
|
675
|
+
f.puts "total_count: #{items.length}"
|
676
|
+
f.puts "error_messages.length: #{error_messages.length}"
|
677
|
+
f.puts "has_errors: #{!error_messages.empty?}"
|
678
|
+
f.puts "condition check: success_count == total_count && !has_errors = #{success_count == items.length && error_messages.empty?}"
|
679
|
+
f.puts ""
|
680
|
+
end
|
681
|
+
|
682
|
+
# 削除結果をフローティングウィンドウで表示
|
683
|
+
show_deletion_result(success_count, items.length, error_messages)
|
684
|
+
|
576
685
|
# 削除完了後の処理
|
577
686
|
@selected_items.clear
|
578
687
|
@directory_listing.refresh if @directory_listing
|
688
|
+
|
689
|
+
true
|
690
|
+
end
|
691
|
+
|
692
|
+
def show_deletion_result(success_count, total_count, error_messages = [])
|
693
|
+
# 詳細デバッグログを出力
|
694
|
+
detailed_debug_file = File.join(Dir.home, '.beniya_detailed_debug.log')
|
695
|
+
File.open(detailed_debug_file, 'a') do |f|
|
696
|
+
f.puts "=== show_deletion_result called #{Time.now} ==="
|
697
|
+
f.puts "Arguments: success_count=#{success_count}, total_count=#{total_count}"
|
698
|
+
f.puts "error_messages: #{error_messages.inspect}"
|
699
|
+
f.puts "error_messages.empty?: #{error_messages.empty?}"
|
700
|
+
f.puts ""
|
701
|
+
end
|
702
|
+
|
703
|
+
# エラーメッセージがある場合はダイアログサイズを拡大
|
704
|
+
has_errors = !error_messages.empty?
|
705
|
+
dialog_width = has_errors ? 50 : 35
|
706
|
+
dialog_height = has_errors ? [8 + error_messages.length, 15].min : 6
|
707
|
+
|
708
|
+
# ダイアログの位置を中央に設定
|
709
|
+
x, y = get_screen_center(dialog_width, dialog_height)
|
710
|
+
|
711
|
+
# 成功・失敗に応じた色設定
|
712
|
+
# デバッグ: success_count == total_count かつ has_errors が false の場合のみ成功扱い
|
713
|
+
if success_count == total_count && !has_errors
|
714
|
+
border_color = "\e[32m" # 緑色(成功)
|
715
|
+
title_color = "\e[1;32m" # 太字緑色
|
716
|
+
title = 'Delete Complete'
|
717
|
+
message = "Deleted #{success_count} item(s)"
|
718
|
+
else
|
719
|
+
border_color = "\e[33m" # 黄色(警告)
|
720
|
+
title_color = "\e[1;33m" # 太字黄色
|
721
|
+
title = 'Delete Result'
|
722
|
+
if success_count == total_count && has_errors
|
723
|
+
# 全て削除成功したがエラーメッセージがある場合(本来ここに入らないはず)
|
724
|
+
message = "#{success_count} deleted (with error info)"
|
725
|
+
else
|
726
|
+
failed_count = total_count - success_count
|
727
|
+
message = "#{success_count} deleted, #{failed_count} failed"
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
# コンテンツの準備
|
732
|
+
content_lines = ['', message]
|
733
|
+
|
734
|
+
# デバッグ情報を追加(開発中のみ)
|
735
|
+
content_lines << ""
|
736
|
+
content_lines << "DEBUG: success=#{success_count}, total=#{total_count}, errors=#{error_messages.length}"
|
737
|
+
|
738
|
+
# エラーメッセージがある場合は追加
|
739
|
+
if has_errors
|
740
|
+
content_lines << ''
|
741
|
+
content_lines << 'Error details:'
|
742
|
+
error_messages.each { |error| content_lines << " #{error}" }
|
743
|
+
end
|
744
|
+
|
745
|
+
content_lines << ''
|
746
|
+
content_lines << 'Press any key to continue...'
|
747
|
+
|
748
|
+
# ダイアログの描画
|
749
|
+
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
750
|
+
border_color: border_color,
|
751
|
+
title_color: title_color,
|
752
|
+
content_color: "\e[37m"
|
753
|
+
})
|
754
|
+
|
755
|
+
# キー入力待機
|
756
|
+
STDIN.getch
|
757
|
+
|
758
|
+
# ダイアログをクリア
|
759
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
760
|
+
@terminal_ui&.refresh_display
|
761
|
+
end
|
762
|
+
|
763
|
+
# フローティングウィンドウの基盤メソッド
|
764
|
+
def draw_floating_window(x, y, width, height, title, content_lines, options = {})
|
765
|
+
# デフォルトオプション
|
766
|
+
border_color = options[:border_color] || "\e[37m" # 白色
|
767
|
+
title_color = options[:title_color] || "\e[1;33m" # 黄色(太字)
|
768
|
+
content_color = options[:content_color] || "\e[37m" # 白色
|
769
|
+
reset_color = "\e[0m"
|
770
|
+
|
771
|
+
# ウィンドウの描画
|
772
|
+
# 上辺
|
773
|
+
print "\e[#{y};#{x}H#{border_color}┌#{'─' * (width - 2)}┐#{reset_color}"
|
774
|
+
|
775
|
+
# タイトル行
|
776
|
+
if title
|
777
|
+
title_width = display_width(title)
|
778
|
+
title_padding = (width - 2 - title_width) / 2
|
779
|
+
padded_title = ' ' * title_padding + title
|
780
|
+
title_line = pad_string_to_width(padded_title, width - 2)
|
781
|
+
print "\e[#{y + 1};#{x}H#{border_color}│#{title_color}#{title_line}#{border_color}│#{reset_color}"
|
782
|
+
|
783
|
+
# タイトル区切り線
|
784
|
+
print "\e[#{y + 2};#{x}H#{border_color}├#{'─' * (width - 2)}┤#{reset_color}"
|
785
|
+
content_start_y = y + 3
|
786
|
+
else
|
787
|
+
content_start_y = y + 1
|
788
|
+
end
|
789
|
+
|
790
|
+
# コンテンツ行
|
791
|
+
content_height = title ? height - 4 : height - 2
|
792
|
+
content_lines.each_with_index do |line, index|
|
793
|
+
break if index >= content_height
|
794
|
+
|
795
|
+
line_y = content_start_y + index
|
796
|
+
line_content = pad_string_to_width(line, width - 2) # 正確な幅でパディング
|
797
|
+
print "\e[#{line_y};#{x}H#{border_color}│#{content_color}#{line_content}#{border_color}│#{reset_color}"
|
798
|
+
end
|
799
|
+
|
800
|
+
# 空行を埋める
|
801
|
+
remaining_lines = content_height - content_lines.length
|
802
|
+
remaining_lines.times do |i|
|
803
|
+
line_y = content_start_y + content_lines.length + i
|
804
|
+
empty_line = ' ' * (width - 2)
|
805
|
+
print "\e[#{line_y};#{x}H#{border_color}│#{empty_line}│#{reset_color}"
|
806
|
+
end
|
807
|
+
|
808
|
+
# 下辺
|
809
|
+
bottom_y = y + height - 1
|
810
|
+
print "\e[#{bottom_y};#{x}H#{border_color}└#{'─' * (width - 2)}┘#{reset_color}"
|
811
|
+
end
|
812
|
+
|
813
|
+
def display_width(str)
|
814
|
+
# 日本語文字の幅を考慮した文字列幅の計算
|
815
|
+
# Unicode East Asian Width プロパティを考慮
|
816
|
+
str.each_char.map do |char|
|
817
|
+
case char
|
818
|
+
when /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\uFF00-\uFFEF]/
|
819
|
+
# 日本語の文字(ひらがな、カタカナ、漢字、全角記号)
|
820
|
+
2
|
821
|
+
when /[\u0020-\u007E]/
|
822
|
+
# ASCII文字
|
823
|
+
1
|
824
|
+
else
|
825
|
+
# その他の文字はバイト数で判断
|
826
|
+
char.bytesize > 1 ? 2 : 1
|
827
|
+
end
|
828
|
+
end.sum
|
829
|
+
end
|
830
|
+
|
831
|
+
def pad_string_to_width(str, target_width)
|
832
|
+
# 文字列を指定した表示幅になるようにパディング
|
833
|
+
current_width = display_width(str)
|
834
|
+
if current_width >= target_width
|
835
|
+
# 文字列が長すぎる場合は切り詰め
|
836
|
+
truncate_to_width(str, target_width)
|
837
|
+
else
|
838
|
+
# 不足分をスペースで埋める
|
839
|
+
str + ' ' * (target_width - current_width)
|
840
|
+
end
|
841
|
+
end
|
842
|
+
|
843
|
+
def truncate_to_width(str, max_width)
|
844
|
+
# 指定した表示幅に収まるように文字列を切り詰め
|
845
|
+
result = ''
|
846
|
+
current_width = 0
|
847
|
+
|
848
|
+
str.each_char do |char|
|
849
|
+
char_width = display_width(char)
|
850
|
+
break if current_width + char_width > max_width
|
851
|
+
|
852
|
+
result += char
|
853
|
+
current_width += char_width
|
854
|
+
end
|
855
|
+
|
856
|
+
result
|
857
|
+
end
|
858
|
+
|
859
|
+
def get_screen_center(content_width, content_height)
|
860
|
+
# ターミナルのサイズを取得
|
861
|
+
console = IO.console
|
862
|
+
if console
|
863
|
+
screen_width, screen_height = console.winsize.reverse
|
864
|
+
else
|
865
|
+
screen_width = 80
|
866
|
+
screen_height = 24
|
867
|
+
end
|
868
|
+
|
869
|
+
# 中央位置を計算
|
870
|
+
x = [(screen_width - content_width) / 2, 1].max
|
871
|
+
y = [(screen_height - content_height) / 2, 1].max
|
872
|
+
|
873
|
+
[x, y]
|
874
|
+
end
|
875
|
+
|
876
|
+
def clear_floating_window_area(x, y, width, height)
|
877
|
+
# フローティングウィンドウの領域をクリア
|
878
|
+
height.times do |row|
|
879
|
+
print "\e[#{y + row};#{x}H#{' ' * width}"
|
880
|
+
end
|
881
|
+
end
|
882
|
+
|
883
|
+
# ブックマーク機能
|
884
|
+
def show_bookmark_menu
|
885
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
579
886
|
|
580
|
-
|
581
|
-
|
887
|
+
# メニューの準備
|
888
|
+
title = 'Bookmark Menu'
|
889
|
+
content_lines = [
|
890
|
+
'',
|
891
|
+
'[A]dd current directory to bookmarks',
|
892
|
+
'[L]ist bookmarks',
|
893
|
+
'[R]emove bookmark',
|
894
|
+
'',
|
895
|
+
'Press 1-9 to go to bookmark directly',
|
896
|
+
'',
|
897
|
+
'Press any other key to cancel'
|
898
|
+
]
|
899
|
+
|
900
|
+
dialog_width = 45
|
901
|
+
dialog_height = 4 + content_lines.length
|
902
|
+
x, y = get_screen_center(dialog_width, dialog_height)
|
903
|
+
|
904
|
+
# ダイアログの描画
|
905
|
+
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
906
|
+
border_color: "\e[34m", # 青色
|
907
|
+
title_color: "\e[1;34m", # 太字青色
|
908
|
+
content_color: "\e[37m" # 白色
|
909
|
+
})
|
910
|
+
|
911
|
+
# キー入力待機
|
912
|
+
loop do
|
913
|
+
input = STDIN.getch.downcase
|
914
|
+
|
915
|
+
case input
|
916
|
+
when 'a'
|
917
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
918
|
+
@terminal_ui&.refresh_display
|
919
|
+
add_bookmark_interactive(current_path)
|
920
|
+
return true
|
921
|
+
when 'l'
|
922
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
923
|
+
@terminal_ui&.refresh_display
|
924
|
+
list_bookmarks_interactive
|
925
|
+
return true
|
926
|
+
when 'r'
|
927
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
928
|
+
@terminal_ui&.refresh_display
|
929
|
+
remove_bookmark_interactive
|
930
|
+
return true
|
931
|
+
when '1', '2', '3', '4', '5', '6', '7', '8', '9'
|
932
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
933
|
+
@terminal_ui&.refresh_display
|
934
|
+
goto_bookmark(input.to_i)
|
935
|
+
return true
|
936
|
+
else
|
937
|
+
# キャンセル
|
938
|
+
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
939
|
+
@terminal_ui&.refresh_display
|
940
|
+
return false
|
941
|
+
end
|
942
|
+
end
|
943
|
+
end
|
944
|
+
|
945
|
+
def add_bookmark_interactive(path)
|
946
|
+
print ConfigLoader.message('bookmark.input_name') || "Enter bookmark name: "
|
947
|
+
name = STDIN.gets.chomp
|
948
|
+
return false if name.empty?
|
949
|
+
|
950
|
+
if @bookmark.add(path, name)
|
951
|
+
puts "\n#{ConfigLoader.message('bookmark.added') || 'Bookmark added'}: #{name}"
|
952
|
+
else
|
953
|
+
puts "\n#{ConfigLoader.message('bookmark.add_failed') || 'Failed to add bookmark'}"
|
954
|
+
end
|
955
|
+
|
956
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
957
|
+
STDIN.getch
|
958
|
+
true
|
959
|
+
end
|
960
|
+
|
961
|
+
def remove_bookmark_interactive
|
962
|
+
bookmarks = @bookmark.list
|
963
|
+
|
964
|
+
if bookmarks.empty?
|
965
|
+
puts "\n#{ConfigLoader.message('bookmark.no_bookmarks') || 'No bookmarks found'}"
|
966
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
967
|
+
STDIN.getch
|
968
|
+
return false
|
969
|
+
end
|
970
|
+
|
971
|
+
puts "\nBookmarks:"
|
972
|
+
bookmarks.each_with_index do |bookmark, index|
|
973
|
+
puts " #{index + 1}. #{bookmark[:name]} (#{bookmark[:path]})"
|
974
|
+
end
|
975
|
+
|
976
|
+
print ConfigLoader.message('bookmark.input_number') || "Enter number to remove: "
|
977
|
+
input = STDIN.gets.chomp
|
978
|
+
number = input.to_i
|
979
|
+
|
980
|
+
if number > 0 && number <= bookmarks.length
|
981
|
+
bookmark_to_remove = bookmarks[number - 1]
|
982
|
+
if @bookmark.remove(bookmark_to_remove[:name])
|
983
|
+
puts "\n#{ConfigLoader.message('bookmark.removed') || 'Bookmark removed'}: #{bookmark_to_remove[:name]}"
|
984
|
+
else
|
985
|
+
puts "\n#{ConfigLoader.message('bookmark.remove_failed') || 'Failed to remove bookmark'}"
|
986
|
+
end
|
987
|
+
else
|
988
|
+
puts "\n#{ConfigLoader.message('bookmark.invalid_number') || 'Invalid number'}"
|
989
|
+
end
|
990
|
+
|
991
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
582
992
|
STDIN.getch
|
583
993
|
true
|
584
994
|
end
|
995
|
+
|
996
|
+
def list_bookmarks_interactive
|
997
|
+
bookmarks = @bookmark.list
|
998
|
+
|
999
|
+
if bookmarks.empty?
|
1000
|
+
puts "\n#{ConfigLoader.message('bookmark.no_bookmarks') || 'No bookmarks found'}"
|
1001
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1002
|
+
STDIN.getch
|
1003
|
+
return false
|
1004
|
+
end
|
1005
|
+
|
1006
|
+
puts "\nBookmarks:"
|
1007
|
+
bookmarks.each_with_index do |bookmark, index|
|
1008
|
+
puts " #{index + 1}. #{bookmark[:name]} (#{bookmark[:path]})"
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1012
|
+
STDIN.getch
|
1013
|
+
true
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
def goto_bookmark(number)
|
1017
|
+
bookmark = @bookmark.find_by_number(number)
|
1018
|
+
|
1019
|
+
unless bookmark
|
1020
|
+
puts "\n#{ConfigLoader.message('bookmark.not_found') || 'Bookmark not found'}: #{number}"
|
1021
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1022
|
+
STDIN.getch
|
1023
|
+
return false
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
unless Dir.exist?(bookmark[:path])
|
1027
|
+
puts "\n#{ConfigLoader.message('bookmark.path_not_exist') || 'Bookmark path does not exist'}: #{bookmark[:path]}"
|
1028
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1029
|
+
STDIN.getch
|
1030
|
+
return false
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
# ディレクトリに移動
|
1034
|
+
result = @directory_listing.navigate_to_path(bookmark[:path])
|
1035
|
+
if result
|
1036
|
+
@current_index = 0
|
1037
|
+
clear_filter_mode
|
1038
|
+
puts "\n#{ConfigLoader.message('bookmark.navigated') || 'Navigated to bookmark'}: #{bookmark[:name]}"
|
1039
|
+
sleep(0.5) # 短時間表示
|
1040
|
+
return true
|
1041
|
+
else
|
1042
|
+
puts "\n#{ConfigLoader.message('bookmark.navigate_failed') || 'Failed to navigate to bookmark'}: #{bookmark[:name]}"
|
1043
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1044
|
+
STDIN.getch
|
1045
|
+
return false
|
1046
|
+
end
|
1047
|
+
end
|
585
1048
|
end
|
586
1049
|
end
|