beniya 0.5.1 → 0.6.1
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 +27 -0
- data/CHANGELOG_v0.6.0.md +182 -0
- data/README.md +55 -3
- data/README_EN.md +83 -3
- data/lib/beniya/application.rb +4 -1
- data/lib/beniya/config.rb +6 -4
- data/lib/beniya/file_preview.rb +11 -3
- data/lib/beniya/health_checker.rb +22 -0
- data/lib/beniya/keybind_handler.rb +223 -504
- data/lib/beniya/terminal_ui.rb +54 -25
- data/lib/beniya/version.rb +1 -1
- metadata +3 -2
@@ -1,23 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'shellwords'
|
4
|
+
require_relative 'file_opener'
|
5
|
+
require_relative 'filter_manager'
|
6
|
+
require_relative 'selection_manager'
|
7
|
+
require_relative 'file_operations'
|
8
|
+
require_relative 'bookmark_manager'
|
9
|
+
require_relative 'zoxide_integration'
|
10
|
+
require_relative 'dialog_renderer'
|
11
|
+
require_relative 'logger'
|
4
12
|
|
5
13
|
module Beniya
|
6
14
|
class KeybindHandler
|
7
|
-
attr_reader :current_index
|
15
|
+
attr_reader :current_index
|
16
|
+
|
17
|
+
def filter_query
|
18
|
+
@filter_manager.filter_query
|
19
|
+
end
|
20
|
+
|
21
|
+
# ASCII character range constants
|
22
|
+
ASCII_PRINTABLE_START = 32
|
23
|
+
ASCII_PRINTABLE_END = 127
|
24
|
+
MULTIBYTE_THRESHOLD = 1
|
25
|
+
|
26
|
+
# Dialog size constants
|
27
|
+
CONFIRMATION_DIALOG_WIDTH = 45
|
28
|
+
DIALOG_BORDER_HEIGHT = 4
|
29
|
+
|
30
|
+
# File system operation constants
|
31
|
+
FILESYSTEM_SYNC_DELAY = 0.01 # 10ms wait for filesystem sync
|
8
32
|
|
9
33
|
def initialize
|
10
34
|
@current_index = 0
|
11
35
|
@directory_listing = nil
|
12
36
|
@terminal_ui = nil
|
13
37
|
@file_opener = FileOpener.new
|
14
|
-
|
15
|
-
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@
|
38
|
+
|
39
|
+
# New manager classes
|
40
|
+
@filter_manager = FilterManager.new
|
41
|
+
@selection_manager = SelectionManager.new
|
42
|
+
@file_operations = FileOperations.new
|
43
|
+
@dialog_renderer = DialogRenderer.new
|
44
|
+
@bookmark_manager = BookmarkManager.new(Bookmark.new, @dialog_renderer)
|
45
|
+
@zoxide_integration = ZoxideIntegration.new(@dialog_renderer)
|
46
|
+
|
47
|
+
# Legacy fields for backward compatibility
|
19
48
|
@base_directory = nil
|
20
|
-
@bookmark = Bookmark.new
|
21
49
|
end
|
22
50
|
|
23
51
|
def set_directory_listing(directory_listing)
|
@@ -34,18 +62,18 @@ module Beniya
|
|
34
62
|
end
|
35
63
|
|
36
64
|
def selected_items
|
37
|
-
@selected_items
|
65
|
+
@selection_manager.selected_items
|
38
66
|
end
|
39
67
|
|
40
68
|
def is_selected?(entry_name)
|
41
|
-
@
|
69
|
+
@selection_manager.selected?(entry_name)
|
42
70
|
end
|
43
71
|
|
44
72
|
def handle_key(key)
|
45
73
|
return false unless @directory_listing
|
46
74
|
|
47
75
|
# フィルターモード中は他のキーバインドを無効化
|
48
|
-
return handle_filter_input(key) if @filter_mode
|
76
|
+
return handle_filter_input(key) if @filter_manager.filter_mode
|
49
77
|
|
50
78
|
case key
|
51
79
|
when 'j'
|
@@ -67,10 +95,9 @@ module Beniya
|
|
67
95
|
when 'e' # e - open directory in file explorer
|
68
96
|
open_directory_in_explorer
|
69
97
|
when 'f' # f - filter files
|
70
|
-
if
|
98
|
+
if @filter_manager.filter_active?
|
71
99
|
# フィルタが設定されている場合は再編集モードに入る
|
72
|
-
@
|
73
|
-
@original_entries = @directory_listing.list_entries.dup if @original_entries.empty?
|
100
|
+
@filter_manager.restart_filter_mode(@directory_listing.list_entries)
|
74
101
|
else
|
75
102
|
# 新規フィルターモード開始
|
76
103
|
start_filter_mode
|
@@ -78,7 +105,7 @@ module Beniya
|
|
78
105
|
when ' ' # Space - toggle selection
|
79
106
|
toggle_selection
|
80
107
|
when "\e" # ESC
|
81
|
-
if
|
108
|
+
if @filter_manager.filter_active?
|
82
109
|
# フィルタが設定されている場合はクリア
|
83
110
|
clear_filter_mode
|
84
111
|
true
|
@@ -105,6 +132,8 @@ module Beniya
|
|
105
132
|
delete_selected_files
|
106
133
|
when 'b' # b - bookmark operations
|
107
134
|
show_bookmark_menu
|
135
|
+
when 'z' # z - zoxide history navigation
|
136
|
+
show_zoxide_menu
|
108
137
|
when '1', '2', '3', '4', '5', '6', '7', '8', '9' # number keys - go to bookmark
|
109
138
|
goto_bookmark(key.to_i)
|
110
139
|
else
|
@@ -123,12 +152,12 @@ module Beniya
|
|
123
152
|
end
|
124
153
|
|
125
154
|
def filter_active?
|
126
|
-
@
|
155
|
+
@filter_manager.filter_active?
|
127
156
|
end
|
128
157
|
|
129
158
|
def get_active_entries
|
130
|
-
if @
|
131
|
-
@
|
159
|
+
if @filter_manager.filter_active?
|
160
|
+
@filter_manager.filtered_entries
|
132
161
|
else
|
133
162
|
@directory_listing&.list_entries || []
|
134
163
|
end
|
@@ -189,10 +218,9 @@ module Beniya
|
|
189
218
|
@terminal_ui&.refresh_display
|
190
219
|
|
191
220
|
@directory_listing.refresh
|
192
|
-
if @
|
221
|
+
if @filter_manager.filter_active?
|
193
222
|
# Re-apply filter with new directory contents
|
194
|
-
@
|
195
|
-
apply_filter
|
223
|
+
@filter_manager.update_entries(@directory_listing.list_entries)
|
196
224
|
else
|
197
225
|
# adjust index to stay within bounds after refresh
|
198
226
|
entries = @directory_listing.list_entries
|
@@ -229,12 +257,16 @@ module Beniya
|
|
229
257
|
current_path = @directory_listing&.current_path || Dir.pwd
|
230
258
|
|
231
259
|
# fzfでファイル検索を実行
|
232
|
-
|
260
|
+
# Dir.chdirを使用してディレクトリ移動を安全に行う
|
261
|
+
selected_file = nil
|
262
|
+
Dir.chdir(current_path) do
|
263
|
+
selected_file = `find . -type f | fzf --preview 'cat {}'`.strip
|
264
|
+
end
|
233
265
|
|
234
266
|
# ファイルが選択された場合、そのファイルを開く
|
235
|
-
if !selected_file.empty?
|
267
|
+
if !selected_file.empty?
|
236
268
|
full_path = File.expand_path(selected_file, current_path)
|
237
|
-
@file_opener.open_file(full_path)
|
269
|
+
@file_opener.open_file(full_path) if File.exist?(full_path)
|
238
270
|
end
|
239
271
|
|
240
272
|
true
|
@@ -255,7 +287,13 @@ module Beniya
|
|
255
287
|
return false if search_query.empty?
|
256
288
|
|
257
289
|
# execute rga file content search
|
258
|
-
|
290
|
+
# Dir.chdirを使用してディレクトリ移動を安全に行う
|
291
|
+
search_results = nil
|
292
|
+
Dir.chdir(current_path) do
|
293
|
+
# Shellwords.escapeで検索クエリをエスケープ
|
294
|
+
escaped_query = Shellwords.escape(search_query)
|
295
|
+
search_results = `rga --line-number --with-filename #{escaped_query} . 2>/dev/null`
|
296
|
+
end
|
259
297
|
|
260
298
|
if search_results.empty?
|
261
299
|
puts "\n#{ConfigLoader.message('keybind.no_matches')}"
|
@@ -288,65 +326,36 @@ module Beniya
|
|
288
326
|
end
|
289
327
|
|
290
328
|
def start_filter_mode
|
291
|
-
@
|
292
|
-
@filter_query = ''
|
293
|
-
@original_entries = @directory_listing.list_entries.dup
|
294
|
-
@filtered_entries = @original_entries.dup
|
329
|
+
@filter_manager.start_filter_mode(@directory_listing.list_entries)
|
295
330
|
@current_index = 0
|
296
331
|
true
|
297
332
|
end
|
298
333
|
|
299
334
|
def handle_filter_input(key)
|
300
|
-
|
301
|
-
|
335
|
+
result = @filter_manager.handle_filter_input(key)
|
336
|
+
|
337
|
+
case result
|
338
|
+
when :exit_clear
|
302
339
|
clear_filter_mode
|
303
|
-
when
|
340
|
+
when :exit_keep
|
304
341
|
exit_filter_mode_keep_filter
|
305
|
-
when
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
else
|
310
|
-
clear_filter_mode
|
311
|
-
end
|
312
|
-
else
|
313
|
-
# printable characters (英数字、記号、日本語文字など)
|
314
|
-
if key.length == 1 && key.ord >= 32 && key.ord < 127 # ASCII printable
|
315
|
-
@filter_query += key
|
316
|
-
apply_filter
|
317
|
-
elsif key.bytesize > 1 # Multi-byte characters (Japanese, etc.)
|
318
|
-
@filter_query += key
|
319
|
-
apply_filter
|
320
|
-
end
|
321
|
-
# その他のキー(Ctrl+c等)は無視
|
342
|
+
when :backspace_exit
|
343
|
+
clear_filter_mode
|
344
|
+
when :continue
|
345
|
+
@current_index = [@current_index, [@filter_manager.filtered_entries.length - 1, 0].max].min
|
322
346
|
end
|
323
|
-
true
|
324
|
-
end
|
325
347
|
|
326
|
-
|
327
|
-
if @filter_query.empty?
|
328
|
-
@filtered_entries = @original_entries.dup
|
329
|
-
else
|
330
|
-
query_downcase = @filter_query.downcase
|
331
|
-
@filtered_entries = @original_entries.select do |entry|
|
332
|
-
entry[:name].downcase.include?(query_downcase)
|
333
|
-
end
|
334
|
-
end
|
335
|
-
@current_index = [@current_index, [@filtered_entries.length - 1, 0].max].min
|
348
|
+
true
|
336
349
|
end
|
337
350
|
|
338
351
|
def exit_filter_mode_keep_filter
|
339
352
|
# フィルタを維持したまま通常モードに戻る
|
340
|
-
@
|
341
|
-
# @filter_query, @filtered_entries は維持
|
353
|
+
@filter_manager.exit_filter_mode_keep_filter
|
342
354
|
end
|
343
355
|
|
344
356
|
def clear_filter_mode
|
345
357
|
# フィルタをクリアして通常モードに戻る
|
346
|
-
@
|
347
|
-
@filter_query = ''
|
348
|
-
@filtered_entries = []
|
349
|
-
@original_entries = []
|
358
|
+
@filter_manager.clear_filter
|
350
359
|
@current_index = 0
|
351
360
|
end
|
352
361
|
|
@@ -363,46 +372,24 @@ module Beniya
|
|
363
372
|
filename = STDIN.gets.chomp
|
364
373
|
return false if filename.empty?
|
365
374
|
|
366
|
-
#
|
367
|
-
|
368
|
-
puts "\n#{ConfigLoader.message('keybind.invalid_filename')}"
|
369
|
-
print ConfigLoader.message('keybind.press_any_key')
|
370
|
-
STDIN.getch
|
371
|
-
return false
|
372
|
-
end
|
373
|
-
|
374
|
-
file_path = File.join(current_path, filename)
|
375
|
-
|
376
|
-
# ファイルが既に存在する場合の確認
|
377
|
-
if File.exist?(file_path)
|
378
|
-
puts "\n#{ConfigLoader.message('keybind.file_exists')}"
|
379
|
-
print ConfigLoader.message('keybind.press_any_key')
|
380
|
-
STDIN.getch
|
381
|
-
return false
|
382
|
-
end
|
383
|
-
|
384
|
-
begin
|
385
|
-
# ファイルを作成
|
386
|
-
File.write(file_path, '')
|
375
|
+
# FileOperationsを使用してファイルを作成
|
376
|
+
result = @file_operations.create_file(current_path, filename)
|
387
377
|
|
388
|
-
|
378
|
+
# ディレクトリ表示を更新
|
379
|
+
if result.success
|
389
380
|
@directory_listing.refresh
|
390
381
|
|
391
382
|
# 作成したファイルを選択状態にする
|
392
383
|
entries = @directory_listing.list_entries
|
393
384
|
new_file_index = entries.find_index { |entry| entry[:name] == filename }
|
394
385
|
@current_index = new_file_index if new_file_index
|
395
|
-
|
396
|
-
puts "\n#{ConfigLoader.message('keybind.file_created')}: #{filename}"
|
397
|
-
print ConfigLoader.message('keybind.press_any_key')
|
398
|
-
STDIN.getch
|
399
|
-
true
|
400
|
-
rescue StandardError => e
|
401
|
-
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
402
|
-
print ConfigLoader.message('keybind.press_any_key')
|
403
|
-
STDIN.getch
|
404
|
-
false
|
405
386
|
end
|
387
|
+
|
388
|
+
# 結果を表示
|
389
|
+
puts "\n#{result.message}"
|
390
|
+
print ConfigLoader.message('keybind.press_any_key')
|
391
|
+
STDIN.getch
|
392
|
+
result.success
|
406
393
|
end
|
407
394
|
|
408
395
|
def create_directory
|
@@ -413,75 +400,63 @@ module Beniya
|
|
413
400
|
dirname = STDIN.gets.chomp
|
414
401
|
return false if dirname.empty?
|
415
402
|
|
416
|
-
#
|
417
|
-
|
418
|
-
puts "\n#{ConfigLoader.message('keybind.invalid_dirname')}"
|
419
|
-
print ConfigLoader.message('keybind.press_any_key')
|
420
|
-
STDIN.getch
|
421
|
-
return false
|
422
|
-
end
|
423
|
-
|
424
|
-
dir_path = File.join(current_path, dirname)
|
425
|
-
|
426
|
-
# ディレクトリが既に存在する場合の確認
|
427
|
-
if File.exist?(dir_path)
|
428
|
-
puts "\n#{ConfigLoader.message('keybind.directory_exists')}"
|
429
|
-
print ConfigLoader.message('keybind.press_any_key')
|
430
|
-
STDIN.getch
|
431
|
-
return false
|
432
|
-
end
|
433
|
-
|
434
|
-
begin
|
435
|
-
# ディレクトリを作成
|
436
|
-
Dir.mkdir(dir_path)
|
403
|
+
# FileOperationsを使用してディレクトリを作成
|
404
|
+
result = @file_operations.create_directory(current_path, dirname)
|
437
405
|
|
438
|
-
|
406
|
+
# ディレクトリ表示を更新
|
407
|
+
if result.success
|
439
408
|
@directory_listing.refresh
|
440
409
|
|
441
410
|
# 作成したディレクトリを選択状態にする
|
442
411
|
entries = @directory_listing.list_entries
|
443
412
|
new_dir_index = entries.find_index { |entry| entry[:name] == dirname }
|
444
413
|
@current_index = new_dir_index if new_dir_index
|
445
|
-
|
446
|
-
puts "\n#{ConfigLoader.message('keybind.directory_created')}: #{dirname}"
|
447
|
-
print ConfigLoader.message('keybind.press_any_key')
|
448
|
-
STDIN.getch
|
449
|
-
true
|
450
|
-
rescue StandardError => e
|
451
|
-
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
452
|
-
print ConfigLoader.message('keybind.press_any_key')
|
453
|
-
STDIN.getch
|
454
|
-
false
|
455
414
|
end
|
415
|
+
|
416
|
+
# 結果を表示
|
417
|
+
puts "\n#{result.message}"
|
418
|
+
print ConfigLoader.message('keybind.press_any_key')
|
419
|
+
STDIN.getch
|
420
|
+
result.success
|
456
421
|
end
|
457
422
|
|
458
423
|
def toggle_selection
|
459
424
|
entry = current_entry
|
460
425
|
return false unless entry
|
461
426
|
|
462
|
-
|
463
|
-
@selected_items.delete(entry[:name])
|
464
|
-
else
|
465
|
-
@selected_items << entry[:name]
|
466
|
-
end
|
427
|
+
@selection_manager.toggle_selection(entry)
|
467
428
|
true
|
468
429
|
end
|
469
430
|
|
470
431
|
def move_selected_to_base
|
471
|
-
return false if @
|
432
|
+
return false if @selection_manager.empty? || @base_directory.nil?
|
433
|
+
|
434
|
+
if show_confirmation_dialog('Move', @selection_manager.count)
|
435
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
436
|
+
result = @file_operations.move(@selection_manager.selected_items, current_path, @base_directory)
|
472
437
|
|
473
|
-
|
474
|
-
|
438
|
+
# Show result and refresh
|
439
|
+
show_operation_result(result)
|
440
|
+
@selection_manager.clear
|
441
|
+
@directory_listing.refresh if @directory_listing
|
442
|
+
true
|
475
443
|
else
|
476
444
|
false
|
477
445
|
end
|
478
446
|
end
|
479
447
|
|
480
448
|
def copy_selected_to_base
|
481
|
-
return false if @
|
449
|
+
return false if @selection_manager.empty? || @base_directory.nil?
|
482
450
|
|
483
|
-
if show_confirmation_dialog('Copy', @
|
484
|
-
|
451
|
+
if show_confirmation_dialog('Copy', @selection_manager.count)
|
452
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
453
|
+
result = @file_operations.copy(@selection_manager.selected_items, current_path, @base_directory)
|
454
|
+
|
455
|
+
# Show result and refresh
|
456
|
+
show_operation_result(result)
|
457
|
+
@selection_manager.clear
|
458
|
+
@directory_listing.refresh if @directory_listing
|
459
|
+
true
|
485
460
|
else
|
486
461
|
false
|
487
462
|
end
|
@@ -493,54 +468,30 @@ module Beniya
|
|
493
468
|
%w[y yes].include?(response)
|
494
469
|
end
|
495
470
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
begin
|
505
|
-
case operation
|
506
|
-
when :move
|
507
|
-
if File.exist?(dest_path)
|
508
|
-
puts "\n#{item_name} already exists in destination. Skipping."
|
509
|
-
next
|
510
|
-
end
|
511
|
-
FileUtils.mv(source_path, dest_path)
|
512
|
-
when :copy
|
513
|
-
if File.exist?(dest_path)
|
514
|
-
puts "\n#{item_name} already exists in destination. Skipping."
|
515
|
-
next
|
516
|
-
end
|
517
|
-
if File.directory?(source_path)
|
518
|
-
FileUtils.cp_r(source_path, dest_path)
|
519
|
-
else
|
520
|
-
FileUtils.cp(source_path, dest_path)
|
521
|
-
end
|
522
|
-
end
|
523
|
-
success_count += 1
|
524
|
-
rescue StandardError => e
|
525
|
-
puts "\nFailed to #{operation == :move ? 'move' : 'copy'} #{item_name}: #{e.message}"
|
526
|
-
end
|
471
|
+
# Helper method to show operation result
|
472
|
+
def show_operation_result(result)
|
473
|
+
if result.errors.any?
|
474
|
+
puts "\n#{result.message}"
|
475
|
+
result.errors.each { |error| puts " - #{error}" }
|
476
|
+
else
|
477
|
+
puts "\n#{result.message}"
|
527
478
|
end
|
528
|
-
|
529
|
-
# 操作完了後の処理
|
530
|
-
@selected_items.clear
|
531
|
-
@directory_listing.refresh if @directory_listing
|
532
|
-
|
533
|
-
puts "\n#{operation == :move ? 'Moved' : 'Copied'} #{success_count} item(s)."
|
534
479
|
print 'Press any key to continue...'
|
535
480
|
STDIN.getch
|
536
|
-
true
|
537
481
|
end
|
538
482
|
|
539
483
|
def delete_selected_files
|
540
|
-
return false if @
|
484
|
+
return false if @selection_manager.empty?
|
541
485
|
|
542
|
-
if show_delete_confirmation(@
|
543
|
-
|
486
|
+
if show_delete_confirmation(@selection_manager.count)
|
487
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
488
|
+
result = @file_operations.delete(@selection_manager.selected_items, current_path)
|
489
|
+
|
490
|
+
# Show detailed delete result
|
491
|
+
show_deletion_result(result.count, @selection_manager.count, result.errors)
|
492
|
+
@selection_manager.clear
|
493
|
+
@directory_listing.refresh if @directory_listing
|
494
|
+
true
|
544
495
|
else
|
545
496
|
false
|
546
497
|
end
|
@@ -563,15 +514,15 @@ module Beniya
|
|
563
514
|
]
|
564
515
|
|
565
516
|
# ダイアログのサイズ設定(コンテンツに合わせて調整)
|
566
|
-
dialog_width =
|
567
|
-
# タイトルあり: 上枠1 + タイトル1 + 区切り1 + コンテンツ
|
568
|
-
dialog_height =
|
517
|
+
dialog_width = CONFIRMATION_DIALOG_WIDTH
|
518
|
+
# タイトルあり: 上枠1 + タイトル1 + 区切り1 + コンテンツ + 下枠1
|
519
|
+
dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
|
569
520
|
|
570
521
|
# ダイアログの位置を中央に設定
|
571
|
-
x, y =
|
522
|
+
x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
|
572
523
|
|
573
524
|
# ダイアログの描画
|
574
|
-
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
525
|
+
@dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
575
526
|
border_color: "\e[31m", # 赤色(警告)
|
576
527
|
title_color: "\e[1;31m", # 太字赤色
|
577
528
|
content_color: "\e[37m" # 白色
|
@@ -587,16 +538,16 @@ module Beniya
|
|
587
538
|
case input
|
588
539
|
when 'y'
|
589
540
|
# ダイアログをクリア
|
590
|
-
|
541
|
+
@dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
|
591
542
|
@terminal_ui&.refresh_display # 画面を再描画
|
592
543
|
return true
|
593
544
|
when 'n', "\e", "\x03" # n, ESC, Ctrl+C
|
594
545
|
# ダイアログをクリア
|
595
|
-
|
546
|
+
@dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
|
596
547
|
@terminal_ui&.refresh_display # 画面を再描画
|
597
548
|
return false
|
598
549
|
when 'q' # qキーでもキャンセル
|
599
|
-
|
550
|
+
@dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
|
600
551
|
@terminal_ui&.refresh_display
|
601
552
|
return false
|
602
553
|
end
|
@@ -605,100 +556,73 @@ module Beniya
|
|
605
556
|
end
|
606
557
|
|
607
558
|
def perform_delete_operation(items)
|
559
|
+
Logger.debug('Starting delete operation', context: { items: items, count: items.length })
|
560
|
+
|
608
561
|
success_count = 0
|
609
562
|
error_messages = []
|
610
563
|
current_path = @directory_listing&.current_path || Dir.pwd
|
611
|
-
debug_log = []
|
612
564
|
|
613
565
|
items.each do |item_name|
|
614
566
|
item_path = File.join(current_path, item_name)
|
615
|
-
|
567
|
+
Logger.debug("Processing deletion", context: { item: item_name, path: item_path })
|
616
568
|
|
617
569
|
begin
|
618
570
|
# ファイル/ディレクトリの存在確認
|
619
571
|
unless File.exist?(item_path)
|
620
572
|
error_messages << "#{item_name}: File not found"
|
621
|
-
|
573
|
+
Logger.warn("File not found for deletion", context: { item: item_name })
|
622
574
|
next
|
623
575
|
end
|
624
576
|
|
625
|
-
debug_log << ' Existence check: OK'
|
626
577
|
is_directory = File.directory?(item_path)
|
627
|
-
|
578
|
+
Logger.debug("Item type determined", context: { item: item_name, type: is_directory ? 'Directory' : 'File' })
|
628
579
|
|
629
580
|
if is_directory
|
630
581
|
FileUtils.rm_rf(item_path)
|
631
|
-
debug_log << ' FileUtils.rm_rf executed'
|
632
582
|
else
|
633
583
|
FileUtils.rm(item_path)
|
634
|
-
debug_log << ' FileUtils.rm executed'
|
635
584
|
end
|
636
585
|
|
637
586
|
# 削除が実際に成功したかを確認
|
638
|
-
sleep(
|
587
|
+
sleep(FILESYSTEM_SYNC_DELAY) # wait for filesystem sync
|
639
588
|
still_exists = File.exist?(item_path)
|
640
|
-
debug_log << " Post-deletion check: #{still_exists}"
|
641
589
|
|
642
590
|
if still_exists
|
643
591
|
error_messages << "#{item_name}: Deletion failed"
|
644
|
-
|
592
|
+
Logger.error("Deletion failed", context: { item: item_name, still_exists: true })
|
645
593
|
else
|
646
594
|
success_count += 1
|
647
|
-
|
595
|
+
Logger.debug("Deletion successful", context: { item: item_name })
|
648
596
|
end
|
649
597
|
rescue StandardError => e
|
650
598
|
error_messages << "#{item_name}: #{e.message}"
|
651
|
-
|
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 ''
|
599
|
+
Logger.error("Exception during deletion", exception: e, context: { item: item_name })
|
666
600
|
end
|
667
601
|
end
|
668
602
|
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
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
|
603
|
+
Logger.debug('Delete operation completed', context: {
|
604
|
+
success_count: success_count,
|
605
|
+
total_count: items.length,
|
606
|
+
error_count: error_messages.length,
|
607
|
+
has_errors: !error_messages.empty?
|
608
|
+
})
|
681
609
|
|
682
610
|
# 削除結果をフローティングウィンドウで表示
|
683
611
|
show_deletion_result(success_count, items.length, error_messages)
|
684
612
|
|
685
613
|
# 削除完了後の処理
|
686
|
-
@
|
614
|
+
@selection_manager.clear
|
687
615
|
@directory_listing.refresh if @directory_listing
|
688
616
|
|
689
617
|
true
|
690
618
|
end
|
691
619
|
|
692
620
|
def show_deletion_result(success_count, total_count, error_messages = [])
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
f.puts "error_messages: #{error_messages.inspect}"
|
699
|
-
f.puts "error_messages.empty?: #{error_messages.empty?}"
|
700
|
-
f.puts ""
|
701
|
-
end
|
621
|
+
Logger.debug('Showing deletion result dialog', context: {
|
622
|
+
success_count: success_count,
|
623
|
+
total_count: total_count,
|
624
|
+
error_messages: error_messages
|
625
|
+
})
|
702
626
|
|
703
627
|
# エラーメッセージがある場合はダイアログサイズを拡大
|
704
628
|
has_errors = !error_messages.empty?
|
@@ -706,10 +630,9 @@ module Beniya
|
|
706
630
|
dialog_height = has_errors ? [8 + error_messages.length, 15].min : 6
|
707
631
|
|
708
632
|
# ダイアログの位置を中央に設定
|
709
|
-
x, y =
|
633
|
+
x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
|
710
634
|
|
711
635
|
# 成功・失敗に応じた色設定
|
712
|
-
# デバッグ: success_count == total_count かつ has_errors が false の場合のみ成功扱い
|
713
636
|
if success_count == total_count && !has_errors
|
714
637
|
border_color = "\e[32m" # 緑色(成功)
|
715
638
|
title_color = "\e[1;32m" # 太字緑色
|
@@ -731,10 +654,6 @@ module Beniya
|
|
731
654
|
# コンテンツの準備
|
732
655
|
content_lines = ['', message]
|
733
656
|
|
734
|
-
# デバッグ情報を追加(開発中のみ)
|
735
|
-
content_lines << ""
|
736
|
-
content_lines << "DEBUG: success=#{success_count}, total=#{total_count}, errors=#{error_messages.length}"
|
737
|
-
|
738
657
|
# エラーメッセージがある場合は追加
|
739
658
|
if has_errors
|
740
659
|
content_lines << ''
|
@@ -746,7 +665,7 @@ module Beniya
|
|
746
665
|
content_lines << 'Press any key to continue...'
|
747
666
|
|
748
667
|
# ダイアログの描画
|
749
|
-
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
668
|
+
@dialog_renderer.draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
750
669
|
border_color: border_color,
|
751
670
|
title_color: title_color,
|
752
671
|
content_color: "\e[37m"
|
@@ -756,294 +675,94 @@ module Beniya
|
|
756
675
|
STDIN.getch
|
757
676
|
|
758
677
|
# ダイアログをクリア
|
759
|
-
|
678
|
+
@dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
|
760
679
|
@terminal_ui&.refresh_display
|
761
680
|
end
|
762
681
|
|
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
682
|
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
683
|
+
# ブックマーク機能
|
684
|
+
def show_bookmark_menu
|
685
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
686
|
+
result = @bookmark_manager.show_menu(current_path)
|
812
687
|
|
813
|
-
|
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
|
688
|
+
@terminal_ui&.refresh_display
|
830
689
|
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
690
|
+
case result[:action]
|
691
|
+
when :add
|
692
|
+
success = @bookmark_manager.add_interactive(result[:path])
|
693
|
+
wait_for_keypress
|
694
|
+
success
|
695
|
+
when :list
|
696
|
+
@bookmark_manager.list_interactive
|
697
|
+
wait_for_keypress
|
698
|
+
true
|
699
|
+
when :remove
|
700
|
+
@bookmark_manager.remove_interactive
|
701
|
+
wait_for_keypress
|
702
|
+
true
|
703
|
+
when :navigate
|
704
|
+
goto_bookmark(result[:number])
|
837
705
|
else
|
838
|
-
|
839
|
-
str + ' ' * (target_width - current_width)
|
706
|
+
false
|
840
707
|
end
|
841
708
|
end
|
842
709
|
|
843
|
-
def
|
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
|
710
|
+
def goto_bookmark(number)
|
711
|
+
bookmark = @bookmark_manager.find_by_number(number)
|
855
712
|
|
856
|
-
|
857
|
-
|
713
|
+
return show_error_and_wait('bookmark.not_found', number) unless bookmark
|
714
|
+
return show_error_and_wait('bookmark.path_not_exist', bookmark[:path]) unless @bookmark_manager.path_exists?(bookmark)
|
858
715
|
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
716
|
+
# ディレクトリに移動
|
717
|
+
if navigate_to_directory(bookmark[:path])
|
718
|
+
puts "\n#{ConfigLoader.message('bookmark.navigated') || 'Navigated to bookmark'}: #{bookmark[:name]}"
|
719
|
+
sleep(0.5) # 短時間表示
|
720
|
+
true
|
864
721
|
else
|
865
|
-
|
866
|
-
screen_height = 24
|
722
|
+
show_error_and_wait('bookmark.navigate_failed', bookmark[:name])
|
867
723
|
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
724
|
end
|
875
725
|
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
end
|
726
|
+
# ヘルパーメソッド
|
727
|
+
def wait_for_keypress
|
728
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
729
|
+
STDIN.getch
|
881
730
|
end
|
882
731
|
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
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
|
732
|
+
def show_error_and_wait(message_key, value)
|
733
|
+
puts "\n#{ConfigLoader.message(message_key) || message_key}: #{value}"
|
734
|
+
wait_for_keypress
|
735
|
+
false
|
943
736
|
end
|
944
737
|
|
945
|
-
def
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
puts "\n#{ConfigLoader.message('bookmark.added') || 'Bookmark added'}: #{name}"
|
738
|
+
def navigate_to_directory(path)
|
739
|
+
result = @directory_listing.navigate_to_path(path)
|
740
|
+
if result
|
741
|
+
@current_index = 0
|
742
|
+
clear_filter_mode
|
743
|
+
true
|
952
744
|
else
|
953
|
-
|
745
|
+
false
|
954
746
|
end
|
955
|
-
|
956
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
957
|
-
STDIN.getch
|
958
|
-
true
|
959
747
|
end
|
960
748
|
|
961
|
-
|
962
|
-
|
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
|
749
|
+
# zoxide 機能
|
750
|
+
def show_zoxide_menu
|
751
|
+
selected_path = @zoxide_integration.show_menu
|
970
752
|
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
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]}"
|
753
|
+
if selected_path && Dir.exist?(selected_path)
|
754
|
+
if navigate_to_directory(selected_path)
|
755
|
+
@zoxide_integration.add_to_history(selected_path)
|
756
|
+
true
|
984
757
|
else
|
985
|
-
|
758
|
+
false
|
986
759
|
end
|
987
760
|
else
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
992
|
-
STDIN.getch
|
993
|
-
true
|
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]})"
|
761
|
+
@terminal_ui&.refresh_display
|
762
|
+
false
|
1009
763
|
end
|
1010
|
-
|
1011
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1012
|
-
STDIN.getch
|
1013
|
-
true
|
1014
764
|
end
|
1015
765
|
|
1016
|
-
|
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
|
766
|
+
private
|
1048
767
|
end
|
1049
768
|
end
|