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.
@@ -1,23 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'bookmark'
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, :filter_query
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
- @filter_mode = false
15
- @filter_query = ''
16
- @filtered_entries = []
17
- @original_entries = []
18
- @selected_items = []
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.dup
65
+ @selection_manager.selected_items
38
66
  end
39
67
 
40
68
  def is_selected?(entry_name)
41
- @selected_items.include?(entry_name)
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 !@filter_query.empty?
98
+ if @filter_manager.filter_active?
71
99
  # フィルタが設定されている場合は再編集モードに入る
72
- @filter_mode = true
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 !@filter_query.empty?
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
- @filter_mode || !@filter_query.empty?
155
+ @filter_manager.filter_active?
127
156
  end
128
157
 
129
158
  def get_active_entries
130
- if @filter_mode || !@filter_query.empty?
131
- @filtered_entries.empty? ? [] : @filtered_entries
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 @filter_mode || !@filter_query.empty?
221
+ if @filter_manager.filter_active?
193
222
  # Re-apply filter with new directory contents
194
- @original_entries = @directory_listing.list_entries.dup
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
- selected_file = `cd "#{current_path}" && find . -type f | fzf --preview 'cat {}'`.strip
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? && File.exist?(File.join(current_path, selected_file))
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
- search_results = `cd "#{current_path}" && rga --line-number --with-filename "#{search_query}" . 2>/dev/null`
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
- @filter_mode = true
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
- case key
301
- when "\e" # ESC - フィルタをクリアして通常モードに戻る
335
+ result = @filter_manager.handle_filter_input(key)
336
+
337
+ case result
338
+ when :exit_clear
302
339
  clear_filter_mode
303
- when "\r", "\n" # Enter - フィルタを維持して通常モードに戻る
340
+ when :exit_keep
304
341
  exit_filter_mode_keep_filter
305
- when "\u007f", "\b" # Backspace
306
- if @filter_query.length > 0
307
- @filter_query = @filter_query[0...-1]
308
- apply_filter
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
- def apply_filter
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
- @filter_mode = false
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
- @filter_mode = false
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
- if filename.include?('/') || filename.include?('\\')
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
- if dirname.include?('/') || dirname.include?('\\')
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
- if @selected_items.include?(entry[:name])
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 @selected_items.empty? || @base_directory.nil?
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
- if show_confirmation_dialog('Move', @selected_items.length)
474
- perform_file_operation(:move, @selected_items, @base_directory)
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 @selected_items.empty? || @base_directory.nil?
449
+ return false if @selection_manager.empty? || @base_directory.nil?
482
450
 
483
- if show_confirmation_dialog('Copy', @selected_items.length)
484
- perform_file_operation(:copy, @selected_items, @base_directory)
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
- def perform_file_operation(operation, items, destination)
497
- success_count = 0
498
- current_path = @directory_listing&.current_path || Dir.pwd
499
-
500
- items.each do |item_name|
501
- source_path = File.join(current_path, item_name)
502
- dest_path = File.join(destination, item_name)
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 @selected_items.empty?
484
+ return false if @selection_manager.empty?
541
485
 
542
- if show_delete_confirmation(@selected_items.length)
543
- perform_delete_operation(@selected_items)
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 = 45
567
- # タイトルあり: 上枠1 + タイトル1 + 区切り1 + コンテンツ6 + 下枠1 = 10
568
- dialog_height = 4 + content_lines.length
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 = get_screen_center(dialog_width, dialog_height)
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
- clear_floating_window_area(x, y, dialog_width, dialog_height)
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
- clear_floating_window_area(x, y, dialog_width, dialog_height)
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
- clear_floating_window_area(x, y, dialog_width, dialog_height)
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
- debug_log << "Processing: #{item_name}"
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
- debug_log << ' Error: File not found'
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
- debug_log << " Type: #{is_directory ? 'Directory' : 'File'}"
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(0.01) # 10ms待機してファイルシステムの同期を待つ
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
- debug_log << ' Result: Failed'
592
+ Logger.error("Deletion failed", context: { item: item_name, still_exists: true })
645
593
  else
646
594
  success_count += 1
647
- debug_log << ' Result: Success'
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
- 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 ''
599
+ Logger.error("Exception during deletion", exception: e, context: { item: item_name })
666
600
  end
667
601
  end
668
602
 
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
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
- @selected_items.clear
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
- 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
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 = get_screen_center(dialog_width, dialog_height)
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
- clear_floating_window_area(x, y, dialog_width, dialog_height)
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
- bottom_y = y + height - 1
810
- print "\e[#{bottom_y};#{x}H#{border_color}└#{'─' * (width - 2)}┘#{reset_color}"
811
- end
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
- 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
688
+ @terminal_ui&.refresh_display
830
689
 
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)
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 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
710
+ def goto_bookmark(number)
711
+ bookmark = @bookmark_manager.find_by_number(number)
855
712
 
856
- result
857
- end
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
- def get_screen_center(content_width, content_height)
860
- # ターミナルのサイズを取得
861
- console = IO.console
862
- if console
863
- screen_width, screen_height = console.winsize.reverse
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
- screen_width = 80
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
- 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
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
- def show_bookmark_menu
885
- current_path = @directory_listing&.current_path || Dir.pwd
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 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}"
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
- puts "\n#{ConfigLoader.message('bookmark.add_failed') || 'Failed to add bookmark'}"
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
- 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
749
+ # zoxide 機能
750
+ def show_zoxide_menu
751
+ selected_path = @zoxide_integration.show_menu
970
752
 
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]}"
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
- puts "\n#{ConfigLoader.message('bookmark.remove_failed') || 'Failed to remove bookmark'}"
758
+ false
986
759
  end
987
760
  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...'
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
- 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
766
+ private
1048
767
  end
1049
768
  end