beniya 0.6.0 → 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/lib/beniya/application.rb +4 -1
- data/lib/beniya/file_preview.rb +11 -3
- data/lib/beniya/keybind_handler.rb +211 -671
- data/lib/beniya/terminal_ui.rb +54 -25
- data/lib/beniya/version.rb +1 -1
- metadata +2 -2
@@ -1,25 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'shellwords'
|
4
|
-
require_relative 'bookmark'
|
5
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'
|
6
12
|
|
7
13
|
module Beniya
|
8
14
|
class KeybindHandler
|
9
|
-
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
|
10
32
|
|
11
33
|
def initialize
|
12
34
|
@current_index = 0
|
13
35
|
@directory_listing = nil
|
14
36
|
@terminal_ui = nil
|
15
37
|
@file_opener = FileOpener.new
|
16
|
-
|
17
|
-
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
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
|
21
48
|
@base_directory = nil
|
22
|
-
@bookmark = Bookmark.new
|
23
49
|
end
|
24
50
|
|
25
51
|
def set_directory_listing(directory_listing)
|
@@ -36,18 +62,18 @@ module Beniya
|
|
36
62
|
end
|
37
63
|
|
38
64
|
def selected_items
|
39
|
-
@selected_items
|
65
|
+
@selection_manager.selected_items
|
40
66
|
end
|
41
67
|
|
42
68
|
def is_selected?(entry_name)
|
43
|
-
@
|
69
|
+
@selection_manager.selected?(entry_name)
|
44
70
|
end
|
45
71
|
|
46
72
|
def handle_key(key)
|
47
73
|
return false unless @directory_listing
|
48
74
|
|
49
75
|
# フィルターモード中は他のキーバインドを無効化
|
50
|
-
return handle_filter_input(key) if @filter_mode
|
76
|
+
return handle_filter_input(key) if @filter_manager.filter_mode
|
51
77
|
|
52
78
|
case key
|
53
79
|
when 'j'
|
@@ -69,10 +95,9 @@ module Beniya
|
|
69
95
|
when 'e' # e - open directory in file explorer
|
70
96
|
open_directory_in_explorer
|
71
97
|
when 'f' # f - filter files
|
72
|
-
if
|
98
|
+
if @filter_manager.filter_active?
|
73
99
|
# フィルタが設定されている場合は再編集モードに入る
|
74
|
-
@
|
75
|
-
@original_entries = @directory_listing.list_entries.dup if @original_entries.empty?
|
100
|
+
@filter_manager.restart_filter_mode(@directory_listing.list_entries)
|
76
101
|
else
|
77
102
|
# 新規フィルターモード開始
|
78
103
|
start_filter_mode
|
@@ -80,7 +105,7 @@ module Beniya
|
|
80
105
|
when ' ' # Space - toggle selection
|
81
106
|
toggle_selection
|
82
107
|
when "\e" # ESC
|
83
|
-
if
|
108
|
+
if @filter_manager.filter_active?
|
84
109
|
# フィルタが設定されている場合はクリア
|
85
110
|
clear_filter_mode
|
86
111
|
true
|
@@ -127,12 +152,12 @@ module Beniya
|
|
127
152
|
end
|
128
153
|
|
129
154
|
def filter_active?
|
130
|
-
@
|
155
|
+
@filter_manager.filter_active?
|
131
156
|
end
|
132
157
|
|
133
158
|
def get_active_entries
|
134
|
-
if @
|
135
|
-
@
|
159
|
+
if @filter_manager.filter_active?
|
160
|
+
@filter_manager.filtered_entries
|
136
161
|
else
|
137
162
|
@directory_listing&.list_entries || []
|
138
163
|
end
|
@@ -193,10 +218,9 @@ module Beniya
|
|
193
218
|
@terminal_ui&.refresh_display
|
194
219
|
|
195
220
|
@directory_listing.refresh
|
196
|
-
if @
|
221
|
+
if @filter_manager.filter_active?
|
197
222
|
# Re-apply filter with new directory contents
|
198
|
-
@
|
199
|
-
apply_filter
|
223
|
+
@filter_manager.update_entries(@directory_listing.list_entries)
|
200
224
|
else
|
201
225
|
# adjust index to stay within bounds after refresh
|
202
226
|
entries = @directory_listing.list_entries
|
@@ -233,12 +257,16 @@ module Beniya
|
|
233
257
|
current_path = @directory_listing&.current_path || Dir.pwd
|
234
258
|
|
235
259
|
# fzfでファイル検索を実行
|
236
|
-
|
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
|
237
265
|
|
238
266
|
# ファイルが選択された場合、そのファイルを開く
|
239
|
-
if !selected_file.empty?
|
267
|
+
if !selected_file.empty?
|
240
268
|
full_path = File.expand_path(selected_file, current_path)
|
241
|
-
@file_opener.open_file(full_path)
|
269
|
+
@file_opener.open_file(full_path) if File.exist?(full_path)
|
242
270
|
end
|
243
271
|
|
244
272
|
true
|
@@ -259,7 +287,13 @@ module Beniya
|
|
259
287
|
return false if search_query.empty?
|
260
288
|
|
261
289
|
# execute rga file content search
|
262
|
-
|
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
|
263
297
|
|
264
298
|
if search_results.empty?
|
265
299
|
puts "\n#{ConfigLoader.message('keybind.no_matches')}"
|
@@ -292,65 +326,36 @@ module Beniya
|
|
292
326
|
end
|
293
327
|
|
294
328
|
def start_filter_mode
|
295
|
-
@
|
296
|
-
@filter_query = ''
|
297
|
-
@original_entries = @directory_listing.list_entries.dup
|
298
|
-
@filtered_entries = @original_entries.dup
|
329
|
+
@filter_manager.start_filter_mode(@directory_listing.list_entries)
|
299
330
|
@current_index = 0
|
300
331
|
true
|
301
332
|
end
|
302
333
|
|
303
334
|
def handle_filter_input(key)
|
304
|
-
|
305
|
-
|
335
|
+
result = @filter_manager.handle_filter_input(key)
|
336
|
+
|
337
|
+
case result
|
338
|
+
when :exit_clear
|
306
339
|
clear_filter_mode
|
307
|
-
when
|
340
|
+
when :exit_keep
|
308
341
|
exit_filter_mode_keep_filter
|
309
|
-
when
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
else
|
314
|
-
clear_filter_mode
|
315
|
-
end
|
316
|
-
else
|
317
|
-
# printable characters (英数字、記号、日本語文字など)
|
318
|
-
if key.length == 1 && key.ord >= 32 && key.ord < 127 # ASCII printable
|
319
|
-
@filter_query += key
|
320
|
-
apply_filter
|
321
|
-
elsif key.bytesize > 1 # Multi-byte characters (Japanese, etc.)
|
322
|
-
@filter_query += key
|
323
|
-
apply_filter
|
324
|
-
end
|
325
|
-
# その他のキー(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
|
326
346
|
end
|
327
|
-
true
|
328
|
-
end
|
329
347
|
|
330
|
-
|
331
|
-
if @filter_query.empty?
|
332
|
-
@filtered_entries = @original_entries.dup
|
333
|
-
else
|
334
|
-
query_downcase = @filter_query.downcase
|
335
|
-
@filtered_entries = @original_entries.select do |entry|
|
336
|
-
entry[:name].downcase.include?(query_downcase)
|
337
|
-
end
|
338
|
-
end
|
339
|
-
@current_index = [@current_index, [@filtered_entries.length - 1, 0].max].min
|
348
|
+
true
|
340
349
|
end
|
341
350
|
|
342
351
|
def exit_filter_mode_keep_filter
|
343
352
|
# フィルタを維持したまま通常モードに戻る
|
344
|
-
@
|
345
|
-
# @filter_query, @filtered_entries は維持
|
353
|
+
@filter_manager.exit_filter_mode_keep_filter
|
346
354
|
end
|
347
355
|
|
348
356
|
def clear_filter_mode
|
349
357
|
# フィルタをクリアして通常モードに戻る
|
350
|
-
@
|
351
|
-
@filter_query = ''
|
352
|
-
@filtered_entries = []
|
353
|
-
@original_entries = []
|
358
|
+
@filter_manager.clear_filter
|
354
359
|
@current_index = 0
|
355
360
|
end
|
356
361
|
|
@@ -367,46 +372,24 @@ module Beniya
|
|
367
372
|
filename = STDIN.gets.chomp
|
368
373
|
return false if filename.empty?
|
369
374
|
|
370
|
-
#
|
371
|
-
|
372
|
-
puts "\n#{ConfigLoader.message('keybind.invalid_filename')}"
|
373
|
-
print ConfigLoader.message('keybind.press_any_key')
|
374
|
-
STDIN.getch
|
375
|
-
return false
|
376
|
-
end
|
377
|
-
|
378
|
-
file_path = File.join(current_path, filename)
|
379
|
-
|
380
|
-
# ファイルが既に存在する場合の確認
|
381
|
-
if File.exist?(file_path)
|
382
|
-
puts "\n#{ConfigLoader.message('keybind.file_exists')}"
|
383
|
-
print ConfigLoader.message('keybind.press_any_key')
|
384
|
-
STDIN.getch
|
385
|
-
return false
|
386
|
-
end
|
387
|
-
|
388
|
-
begin
|
389
|
-
# ファイルを作成
|
390
|
-
File.write(file_path, '')
|
375
|
+
# FileOperationsを使用してファイルを作成
|
376
|
+
result = @file_operations.create_file(current_path, filename)
|
391
377
|
|
392
|
-
|
378
|
+
# ディレクトリ表示を更新
|
379
|
+
if result.success
|
393
380
|
@directory_listing.refresh
|
394
381
|
|
395
382
|
# 作成したファイルを選択状態にする
|
396
383
|
entries = @directory_listing.list_entries
|
397
384
|
new_file_index = entries.find_index { |entry| entry[:name] == filename }
|
398
385
|
@current_index = new_file_index if new_file_index
|
399
|
-
|
400
|
-
puts "\n#{ConfigLoader.message('keybind.file_created')}: #{filename}"
|
401
|
-
print ConfigLoader.message('keybind.press_any_key')
|
402
|
-
STDIN.getch
|
403
|
-
true
|
404
|
-
rescue StandardError => e
|
405
|
-
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
406
|
-
print ConfigLoader.message('keybind.press_any_key')
|
407
|
-
STDIN.getch
|
408
|
-
false
|
409
386
|
end
|
387
|
+
|
388
|
+
# 結果を表示
|
389
|
+
puts "\n#{result.message}"
|
390
|
+
print ConfigLoader.message('keybind.press_any_key')
|
391
|
+
STDIN.getch
|
392
|
+
result.success
|
410
393
|
end
|
411
394
|
|
412
395
|
def create_directory
|
@@ -417,75 +400,63 @@ module Beniya
|
|
417
400
|
dirname = STDIN.gets.chomp
|
418
401
|
return false if dirname.empty?
|
419
402
|
|
420
|
-
#
|
421
|
-
|
422
|
-
puts "\n#{ConfigLoader.message('keybind.invalid_dirname')}"
|
423
|
-
print ConfigLoader.message('keybind.press_any_key')
|
424
|
-
STDIN.getch
|
425
|
-
return false
|
426
|
-
end
|
427
|
-
|
428
|
-
dir_path = File.join(current_path, dirname)
|
403
|
+
# FileOperationsを使用してディレクトリを作成
|
404
|
+
result = @file_operations.create_directory(current_path, dirname)
|
429
405
|
|
430
|
-
#
|
431
|
-
if
|
432
|
-
puts "\n#{ConfigLoader.message('keybind.directory_exists')}"
|
433
|
-
print ConfigLoader.message('keybind.press_any_key')
|
434
|
-
STDIN.getch
|
435
|
-
return false
|
436
|
-
end
|
437
|
-
|
438
|
-
begin
|
439
|
-
# ディレクトリを作成
|
440
|
-
Dir.mkdir(dir_path)
|
441
|
-
|
442
|
-
# ディレクトリ表示を更新
|
406
|
+
# ディレクトリ表示を更新
|
407
|
+
if result.success
|
443
408
|
@directory_listing.refresh
|
444
409
|
|
445
410
|
# 作成したディレクトリを選択状態にする
|
446
411
|
entries = @directory_listing.list_entries
|
447
412
|
new_dir_index = entries.find_index { |entry| entry[:name] == dirname }
|
448
413
|
@current_index = new_dir_index if new_dir_index
|
449
|
-
|
450
|
-
puts "\n#{ConfigLoader.message('keybind.directory_created')}: #{dirname}"
|
451
|
-
print ConfigLoader.message('keybind.press_any_key')
|
452
|
-
STDIN.getch
|
453
|
-
true
|
454
|
-
rescue StandardError => e
|
455
|
-
puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
|
456
|
-
print ConfigLoader.message('keybind.press_any_key')
|
457
|
-
STDIN.getch
|
458
|
-
false
|
459
414
|
end
|
415
|
+
|
416
|
+
# 結果を表示
|
417
|
+
puts "\n#{result.message}"
|
418
|
+
print ConfigLoader.message('keybind.press_any_key')
|
419
|
+
STDIN.getch
|
420
|
+
result.success
|
460
421
|
end
|
461
422
|
|
462
423
|
def toggle_selection
|
463
424
|
entry = current_entry
|
464
425
|
return false unless entry
|
465
426
|
|
466
|
-
|
467
|
-
@selected_items.delete(entry[:name])
|
468
|
-
else
|
469
|
-
@selected_items << entry[:name]
|
470
|
-
end
|
427
|
+
@selection_manager.toggle_selection(entry)
|
471
428
|
true
|
472
429
|
end
|
473
430
|
|
474
431
|
def move_selected_to_base
|
475
|
-
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)
|
476
437
|
|
477
|
-
|
478
|
-
|
438
|
+
# Show result and refresh
|
439
|
+
show_operation_result(result)
|
440
|
+
@selection_manager.clear
|
441
|
+
@directory_listing.refresh if @directory_listing
|
442
|
+
true
|
479
443
|
else
|
480
444
|
false
|
481
445
|
end
|
482
446
|
end
|
483
447
|
|
484
448
|
def copy_selected_to_base
|
485
|
-
return false if @
|
449
|
+
return false if @selection_manager.empty? || @base_directory.nil?
|
450
|
+
|
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)
|
486
454
|
|
487
|
-
|
488
|
-
|
455
|
+
# Show result and refresh
|
456
|
+
show_operation_result(result)
|
457
|
+
@selection_manager.clear
|
458
|
+
@directory_listing.refresh if @directory_listing
|
459
|
+
true
|
489
460
|
else
|
490
461
|
false
|
491
462
|
end
|
@@ -497,54 +468,30 @@ module Beniya
|
|
497
468
|
%w[y yes].include?(response)
|
498
469
|
end
|
499
470
|
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
begin
|
509
|
-
case operation
|
510
|
-
when :move
|
511
|
-
if File.exist?(dest_path)
|
512
|
-
puts "\n#{item_name} already exists in destination. Skipping."
|
513
|
-
next
|
514
|
-
end
|
515
|
-
FileUtils.mv(source_path, dest_path)
|
516
|
-
when :copy
|
517
|
-
if File.exist?(dest_path)
|
518
|
-
puts "\n#{item_name} already exists in destination. Skipping."
|
519
|
-
next
|
520
|
-
end
|
521
|
-
if File.directory?(source_path)
|
522
|
-
FileUtils.cp_r(source_path, dest_path)
|
523
|
-
else
|
524
|
-
FileUtils.cp(source_path, dest_path)
|
525
|
-
end
|
526
|
-
end
|
527
|
-
success_count += 1
|
528
|
-
rescue StandardError => e
|
529
|
-
puts "\nFailed to #{operation == :move ? 'move' : 'copy'} #{item_name}: #{e.message}"
|
530
|
-
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}"
|
531
478
|
end
|
532
|
-
|
533
|
-
# 操作完了後の処理
|
534
|
-
@selected_items.clear
|
535
|
-
@directory_listing.refresh if @directory_listing
|
536
|
-
|
537
|
-
puts "\n#{operation == :move ? 'Moved' : 'Copied'} #{success_count} item(s)."
|
538
479
|
print 'Press any key to continue...'
|
539
480
|
STDIN.getch
|
540
|
-
true
|
541
481
|
end
|
542
482
|
|
543
483
|
def delete_selected_files
|
544
|
-
return false if @
|
484
|
+
return false if @selection_manager.empty?
|
485
|
+
|
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)
|
545
489
|
|
546
|
-
|
547
|
-
|
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
|
548
495
|
else
|
549
496
|
false
|
550
497
|
end
|
@@ -567,15 +514,15 @@ module Beniya
|
|
567
514
|
]
|
568
515
|
|
569
516
|
# ダイアログのサイズ設定(コンテンツに合わせて調整)
|
570
|
-
dialog_width =
|
571
|
-
# タイトルあり: 上枠1 + タイトル1 + 区切り1 + コンテンツ
|
572
|
-
dialog_height =
|
517
|
+
dialog_width = CONFIRMATION_DIALOG_WIDTH
|
518
|
+
# タイトルあり: 上枠1 + タイトル1 + 区切り1 + コンテンツ + 下枠1
|
519
|
+
dialog_height = DIALOG_BORDER_HEIGHT + content_lines.length
|
573
520
|
|
574
521
|
# ダイアログの位置を中央に設定
|
575
|
-
x, y =
|
522
|
+
x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
|
576
523
|
|
577
524
|
# ダイアログの描画
|
578
|
-
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, {
|
579
526
|
border_color: "\e[31m", # 赤色(警告)
|
580
527
|
title_color: "\e[1;31m", # 太字赤色
|
581
528
|
content_color: "\e[37m" # 白色
|
@@ -591,16 +538,16 @@ module Beniya
|
|
591
538
|
case input
|
592
539
|
when 'y'
|
593
540
|
# ダイアログをクリア
|
594
|
-
|
541
|
+
@dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
|
595
542
|
@terminal_ui&.refresh_display # 画面を再描画
|
596
543
|
return true
|
597
544
|
when 'n', "\e", "\x03" # n, ESC, Ctrl+C
|
598
545
|
# ダイアログをクリア
|
599
|
-
|
546
|
+
@dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
|
600
547
|
@terminal_ui&.refresh_display # 画面を再描画
|
601
548
|
return false
|
602
549
|
when 'q' # qキーでもキャンセル
|
603
|
-
|
550
|
+
@dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
|
604
551
|
@terminal_ui&.refresh_display
|
605
552
|
return false
|
606
553
|
end
|
@@ -609,100 +556,73 @@ module Beniya
|
|
609
556
|
end
|
610
557
|
|
611
558
|
def perform_delete_operation(items)
|
559
|
+
Logger.debug('Starting delete operation', context: { items: items, count: items.length })
|
560
|
+
|
612
561
|
success_count = 0
|
613
562
|
error_messages = []
|
614
563
|
current_path = @directory_listing&.current_path || Dir.pwd
|
615
|
-
debug_log = []
|
616
564
|
|
617
565
|
items.each do |item_name|
|
618
566
|
item_path = File.join(current_path, item_name)
|
619
|
-
|
567
|
+
Logger.debug("Processing deletion", context: { item: item_name, path: item_path })
|
620
568
|
|
621
569
|
begin
|
622
570
|
# ファイル/ディレクトリの存在確認
|
623
571
|
unless File.exist?(item_path)
|
624
572
|
error_messages << "#{item_name}: File not found"
|
625
|
-
|
573
|
+
Logger.warn("File not found for deletion", context: { item: item_name })
|
626
574
|
next
|
627
575
|
end
|
628
576
|
|
629
|
-
debug_log << ' Existence check: OK'
|
630
577
|
is_directory = File.directory?(item_path)
|
631
|
-
|
578
|
+
Logger.debug("Item type determined", context: { item: item_name, type: is_directory ? 'Directory' : 'File' })
|
632
579
|
|
633
580
|
if is_directory
|
634
581
|
FileUtils.rm_rf(item_path)
|
635
|
-
debug_log << ' FileUtils.rm_rf executed'
|
636
582
|
else
|
637
583
|
FileUtils.rm(item_path)
|
638
|
-
debug_log << ' FileUtils.rm executed'
|
639
584
|
end
|
640
585
|
|
641
586
|
# 削除が実際に成功したかを確認
|
642
|
-
sleep(
|
587
|
+
sleep(FILESYSTEM_SYNC_DELAY) # wait for filesystem sync
|
643
588
|
still_exists = File.exist?(item_path)
|
644
|
-
debug_log << " Post-deletion check: #{still_exists}"
|
645
589
|
|
646
590
|
if still_exists
|
647
591
|
error_messages << "#{item_name}: Deletion failed"
|
648
|
-
|
592
|
+
Logger.error("Deletion failed", context: { item: item_name, still_exists: true })
|
649
593
|
else
|
650
594
|
success_count += 1
|
651
|
-
|
595
|
+
Logger.debug("Deletion successful", context: { item: item_name })
|
652
596
|
end
|
653
597
|
rescue StandardError => e
|
654
598
|
error_messages << "#{item_name}: #{e.message}"
|
655
|
-
|
599
|
+
Logger.error("Exception during deletion", exception: e, context: { item: item_name })
|
656
600
|
end
|
657
601
|
end
|
658
602
|
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
f.puts "Target items: #{items.inspect}"
|
666
|
-
debug_log.each { |line| f.puts line }
|
667
|
-
f.puts "Final result: #{success_count} successful, #{items.length - success_count} failed"
|
668
|
-
f.puts "Error messages: #{error_messages.inspect}"
|
669
|
-
f.puts ''
|
670
|
-
end
|
671
|
-
end
|
672
|
-
|
673
|
-
|
674
|
-
# デバッグ用:削除結果の値をログファイルに出力
|
675
|
-
result_debug_file = File.join(Dir.home, '.beniya_result_debug.log')
|
676
|
-
File.open(result_debug_file, 'a') do |f|
|
677
|
-
f.puts "=== Delete Result Debug #{Time.now} ==="
|
678
|
-
f.puts "success_count: #{success_count}"
|
679
|
-
f.puts "total_count: #{items.length}"
|
680
|
-
f.puts "error_messages.length: #{error_messages.length}"
|
681
|
-
f.puts "has_errors: #{!error_messages.empty?}"
|
682
|
-
f.puts "condition check: success_count == total_count && !has_errors = #{success_count == items.length && error_messages.empty?}"
|
683
|
-
f.puts ""
|
684
|
-
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
|
+
})
|
685
609
|
|
686
610
|
# 削除結果をフローティングウィンドウで表示
|
687
611
|
show_deletion_result(success_count, items.length, error_messages)
|
688
612
|
|
689
613
|
# 削除完了後の処理
|
690
|
-
@
|
614
|
+
@selection_manager.clear
|
691
615
|
@directory_listing.refresh if @directory_listing
|
692
616
|
|
693
617
|
true
|
694
618
|
end
|
695
619
|
|
696
620
|
def show_deletion_result(success_count, total_count, error_messages = [])
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
f.puts "error_messages: #{error_messages.inspect}"
|
703
|
-
f.puts "error_messages.empty?: #{error_messages.empty?}"
|
704
|
-
f.puts ""
|
705
|
-
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
|
+
})
|
706
626
|
|
707
627
|
# エラーメッセージがある場合はダイアログサイズを拡大
|
708
628
|
has_errors = !error_messages.empty?
|
@@ -710,10 +630,9 @@ module Beniya
|
|
710
630
|
dialog_height = has_errors ? [8 + error_messages.length, 15].min : 6
|
711
631
|
|
712
632
|
# ダイアログの位置を中央に設定
|
713
|
-
x, y =
|
633
|
+
x, y = @dialog_renderer.calculate_center(dialog_width, dialog_height)
|
714
634
|
|
715
635
|
# 成功・失敗に応じた色設定
|
716
|
-
# デバッグ: success_count == total_count かつ has_errors が false の場合のみ成功扱い
|
717
636
|
if success_count == total_count && !has_errors
|
718
637
|
border_color = "\e[32m" # 緑色(成功)
|
719
638
|
title_color = "\e[1;32m" # 太字緑色
|
@@ -735,10 +654,6 @@ module Beniya
|
|
735
654
|
# コンテンツの準備
|
736
655
|
content_lines = ['', message]
|
737
656
|
|
738
|
-
# デバッグ情報を追加(開発中のみ)
|
739
|
-
content_lines << ""
|
740
|
-
content_lines << "DEBUG: success=#{success_count}, total=#{total_count}, errors=#{error_messages.length}"
|
741
|
-
|
742
657
|
# エラーメッセージがある場合は追加
|
743
658
|
if has_errors
|
744
659
|
content_lines << ''
|
@@ -750,7 +665,7 @@ module Beniya
|
|
750
665
|
content_lines << 'Press any key to continue...'
|
751
666
|
|
752
667
|
# ダイアログの描画
|
753
|
-
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, {
|
754
669
|
border_color: border_color,
|
755
670
|
title_color: title_color,
|
756
671
|
content_color: "\e[37m"
|
@@ -760,469 +675,94 @@ module Beniya
|
|
760
675
|
STDIN.getch
|
761
676
|
|
762
677
|
# ダイアログをクリア
|
763
|
-
|
678
|
+
@dialog_renderer.clear_area(x, y, dialog_width, dialog_height)
|
764
679
|
@terminal_ui&.refresh_display
|
765
680
|
end
|
766
681
|
|
767
|
-
# フローティングウィンドウの基盤メソッド
|
768
|
-
def draw_floating_window(x, y, width, height, title, content_lines, options = {})
|
769
|
-
# デフォルトオプション
|
770
|
-
border_color = options[:border_color] || "\e[37m" # 白色
|
771
|
-
title_color = options[:title_color] || "\e[1;33m" # 黄色(太字)
|
772
|
-
content_color = options[:content_color] || "\e[37m" # 白色
|
773
|
-
reset_color = "\e[0m"
|
774
|
-
|
775
|
-
# ウィンドウの描画
|
776
|
-
# 上辺
|
777
|
-
print "\e[#{y};#{x}H#{border_color}┌#{'─' * (width - 2)}┐#{reset_color}"
|
778
|
-
|
779
|
-
# タイトル行
|
780
|
-
if title
|
781
|
-
title_width = display_width(title)
|
782
|
-
title_padding = (width - 2 - title_width) / 2
|
783
|
-
padded_title = ' ' * title_padding + title
|
784
|
-
title_line = pad_string_to_width(padded_title, width - 2)
|
785
|
-
print "\e[#{y + 1};#{x}H#{border_color}│#{title_color}#{title_line}#{border_color}│#{reset_color}"
|
786
|
-
|
787
|
-
# タイトル区切り線
|
788
|
-
print "\e[#{y + 2};#{x}H#{border_color}├#{'─' * (width - 2)}┤#{reset_color}"
|
789
|
-
content_start_y = y + 3
|
790
|
-
else
|
791
|
-
content_start_y = y + 1
|
792
|
-
end
|
793
|
-
|
794
|
-
# コンテンツ行
|
795
|
-
content_height = title ? height - 4 : height - 2
|
796
|
-
content_lines.each_with_index do |line, index|
|
797
|
-
break if index >= content_height
|
798
|
-
|
799
|
-
line_y = content_start_y + index
|
800
|
-
line_content = pad_string_to_width(line, width - 2) # 正確な幅でパディング
|
801
|
-
print "\e[#{line_y};#{x}H#{border_color}│#{content_color}#{line_content}#{border_color}│#{reset_color}"
|
802
|
-
end
|
803
|
-
|
804
|
-
# 空行を埋める
|
805
|
-
remaining_lines = content_height - content_lines.length
|
806
|
-
remaining_lines.times do |i|
|
807
|
-
line_y = content_start_y + content_lines.length + i
|
808
|
-
empty_line = ' ' * (width - 2)
|
809
|
-
print "\e[#{line_y};#{x}H#{border_color}│#{empty_line}│#{reset_color}"
|
810
|
-
end
|
811
|
-
|
812
|
-
# 下辺
|
813
|
-
bottom_y = y + height - 1
|
814
|
-
print "\e[#{bottom_y};#{x}H#{border_color}└#{'─' * (width - 2)}┘#{reset_color}"
|
815
|
-
end
|
816
|
-
|
817
|
-
def display_width(str)
|
818
|
-
# 日本語文字の幅を考慮した文字列幅の計算
|
819
|
-
# Unicode East Asian Width プロパティを考慮
|
820
|
-
str.each_char.map do |char|
|
821
|
-
case char
|
822
|
-
when /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\uFF00-\uFFEF]/
|
823
|
-
# 日本語の文字(ひらがな、カタカナ、漢字、全角記号)
|
824
|
-
2
|
825
|
-
when /[\u0020-\u007E]/
|
826
|
-
# ASCII文字
|
827
|
-
1
|
828
|
-
else
|
829
|
-
# その他の文字はバイト数で判断
|
830
|
-
char.bytesize > 1 ? 2 : 1
|
831
|
-
end
|
832
|
-
end.sum
|
833
|
-
end
|
834
|
-
|
835
|
-
def pad_string_to_width(str, target_width)
|
836
|
-
# 文字列を指定した表示幅になるようにパディング
|
837
|
-
current_width = display_width(str)
|
838
|
-
if current_width >= target_width
|
839
|
-
# 文字列が長すぎる場合は切り詰め
|
840
|
-
truncate_to_width(str, target_width)
|
841
|
-
else
|
842
|
-
# 不足分をスペースで埋める
|
843
|
-
str + ' ' * (target_width - current_width)
|
844
|
-
end
|
845
|
-
end
|
846
|
-
|
847
|
-
def truncate_to_width(str, max_width)
|
848
|
-
# 指定した表示幅に収まるように文字列を切り詰め
|
849
|
-
result = ''
|
850
|
-
current_width = 0
|
851
|
-
|
852
|
-
str.each_char do |char|
|
853
|
-
char_width = display_width(char)
|
854
|
-
break if current_width + char_width > max_width
|
855
|
-
|
856
|
-
result += char
|
857
|
-
current_width += char_width
|
858
|
-
end
|
859
|
-
|
860
|
-
result
|
861
|
-
end
|
862
|
-
|
863
|
-
def get_screen_center(content_width, content_height)
|
864
|
-
# ターミナルのサイズを取得
|
865
|
-
console = IO.console
|
866
|
-
if console
|
867
|
-
screen_width, screen_height = console.winsize.reverse
|
868
|
-
else
|
869
|
-
screen_width = 80
|
870
|
-
screen_height = 24
|
871
|
-
end
|
872
|
-
|
873
|
-
# 中央位置を計算
|
874
|
-
x = [(screen_width - content_width) / 2, 1].max
|
875
|
-
y = [(screen_height - content_height) / 2, 1].max
|
876
|
-
|
877
|
-
[x, y]
|
878
|
-
end
|
879
|
-
|
880
|
-
def clear_floating_window_area(x, y, width, height)
|
881
|
-
# フローティングウィンドウの領域をクリア
|
882
|
-
height.times do |row|
|
883
|
-
print "\e[#{y + row};#{x}H#{' ' * width}"
|
884
|
-
end
|
885
|
-
end
|
886
682
|
|
887
683
|
# ブックマーク機能
|
888
684
|
def show_bookmark_menu
|
889
685
|
current_path = @directory_listing&.current_path || Dir.pwd
|
890
|
-
|
891
|
-
# メニューの準備
|
892
|
-
title = 'Bookmark Menu'
|
893
|
-
content_lines = [
|
894
|
-
'',
|
895
|
-
'[A]dd current directory to bookmarks',
|
896
|
-
'[L]ist bookmarks',
|
897
|
-
'[R]emove bookmark',
|
898
|
-
'',
|
899
|
-
'Press 1-9 to go to bookmark directly',
|
900
|
-
'',
|
901
|
-
'Press any other key to cancel'
|
902
|
-
]
|
903
|
-
|
904
|
-
dialog_width = 45
|
905
|
-
dialog_height = 4 + content_lines.length
|
906
|
-
x, y = get_screen_center(dialog_width, dialog_height)
|
907
|
-
|
908
|
-
# ダイアログの描画
|
909
|
-
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
910
|
-
border_color: "\e[34m", # 青色
|
911
|
-
title_color: "\e[1;34m", # 太字青色
|
912
|
-
content_color: "\e[37m" # 白色
|
913
|
-
})
|
686
|
+
result = @bookmark_manager.show_menu(current_path)
|
914
687
|
|
915
|
-
|
916
|
-
loop do
|
917
|
-
input = STDIN.getch.downcase
|
918
|
-
|
919
|
-
case input
|
920
|
-
when 'a'
|
921
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
922
|
-
@terminal_ui&.refresh_display
|
923
|
-
add_bookmark_interactive(current_path)
|
924
|
-
return true
|
925
|
-
when 'l'
|
926
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
927
|
-
@terminal_ui&.refresh_display
|
928
|
-
list_bookmarks_interactive
|
929
|
-
return true
|
930
|
-
when 'r'
|
931
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
932
|
-
@terminal_ui&.refresh_display
|
933
|
-
remove_bookmark_interactive
|
934
|
-
return true
|
935
|
-
when '1', '2', '3', '4', '5', '6', '7', '8', '9'
|
936
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
937
|
-
@terminal_ui&.refresh_display
|
938
|
-
goto_bookmark(input.to_i)
|
939
|
-
return true
|
940
|
-
else
|
941
|
-
# キャンセル
|
942
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
943
|
-
@terminal_ui&.refresh_display
|
944
|
-
return false
|
945
|
-
end
|
946
|
-
end
|
947
|
-
end
|
948
|
-
|
949
|
-
def add_bookmark_interactive(path)
|
950
|
-
print ConfigLoader.message('bookmark.input_name') || "Enter bookmark name: "
|
951
|
-
name = STDIN.gets.chomp
|
952
|
-
return false if name.empty?
|
688
|
+
@terminal_ui&.refresh_display
|
953
689
|
|
954
|
-
|
955
|
-
|
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])
|
956
705
|
else
|
957
|
-
|
706
|
+
false
|
958
707
|
end
|
959
|
-
|
960
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
961
|
-
STDIN.getch
|
962
|
-
true
|
963
708
|
end
|
964
709
|
|
965
|
-
def
|
966
|
-
|
967
|
-
|
968
|
-
if bookmarks.empty?
|
969
|
-
puts "\n#{ConfigLoader.message('bookmark.no_bookmarks') || 'No bookmarks found'}"
|
970
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
971
|
-
STDIN.getch
|
972
|
-
return false
|
973
|
-
end
|
710
|
+
def goto_bookmark(number)
|
711
|
+
bookmark = @bookmark_manager.find_by_number(number)
|
974
712
|
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
if number > 0 && number <= bookmarks.length
|
985
|
-
bookmark_to_remove = bookmarks[number - 1]
|
986
|
-
if @bookmark.remove(bookmark_to_remove[:name])
|
987
|
-
puts "\n#{ConfigLoader.message('bookmark.removed') || 'Bookmark removed'}: #{bookmark_to_remove[:name]}"
|
988
|
-
else
|
989
|
-
puts "\n#{ConfigLoader.message('bookmark.remove_failed') || 'Failed to remove bookmark'}"
|
990
|
-
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)
|
715
|
+
|
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
|
991
721
|
else
|
992
|
-
|
722
|
+
show_error_and_wait('bookmark.navigate_failed', bookmark[:name])
|
993
723
|
end
|
994
|
-
|
995
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
996
|
-
STDIN.getch
|
997
|
-
true
|
998
724
|
end
|
999
725
|
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
if bookmarks.empty?
|
1004
|
-
puts "\n#{ConfigLoader.message('bookmark.no_bookmarks') || 'No bookmarks found'}"
|
1005
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1006
|
-
STDIN.getch
|
1007
|
-
return false
|
1008
|
-
end
|
1009
|
-
|
1010
|
-
puts "\nBookmarks:"
|
1011
|
-
bookmarks.each_with_index do |bookmark, index|
|
1012
|
-
puts " #{index + 1}. #{bookmark[:name]} (#{bookmark[:path]})"
|
1013
|
-
end
|
1014
|
-
|
726
|
+
# ヘルパーメソッド
|
727
|
+
def wait_for_keypress
|
1015
728
|
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1016
729
|
STDIN.getch
|
1017
|
-
true
|
1018
730
|
end
|
1019
731
|
|
1020
|
-
def
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1026
|
-
STDIN.getch
|
1027
|
-
return false
|
1028
|
-
end
|
1029
|
-
|
1030
|
-
unless Dir.exist?(bookmark[:path])
|
1031
|
-
puts "\n#{ConfigLoader.message('bookmark.path_not_exist') || 'Bookmark path does not exist'}: #{bookmark[:path]}"
|
1032
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1033
|
-
STDIN.getch
|
1034
|
-
return false
|
1035
|
-
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
|
736
|
+
end
|
1036
737
|
|
1037
|
-
|
1038
|
-
result = @directory_listing.navigate_to_path(
|
738
|
+
def navigate_to_directory(path)
|
739
|
+
result = @directory_listing.navigate_to_path(path)
|
1039
740
|
if result
|
1040
741
|
@current_index = 0
|
1041
742
|
clear_filter_mode
|
1042
|
-
|
1043
|
-
sleep(0.5) # 短時間表示
|
1044
|
-
return true
|
743
|
+
true
|
1045
744
|
else
|
1046
|
-
|
1047
|
-
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
1048
|
-
STDIN.getch
|
1049
|
-
return false
|
745
|
+
false
|
1050
746
|
end
|
1051
747
|
end
|
1052
748
|
|
1053
749
|
# zoxide 機能
|
1054
750
|
def show_zoxide_menu
|
1055
|
-
|
1056
|
-
|
1057
|
-
if history.empty?
|
1058
|
-
show_no_zoxide_history_message
|
1059
|
-
return false
|
1060
|
-
end
|
751
|
+
selected_path = @zoxide_integration.show_menu
|
1061
752
|
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
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
|
757
|
+
else
|
758
|
+
false
|
759
|
+
end
|
1067
760
|
else
|
761
|
+
@terminal_ui&.refresh_display
|
1068
762
|
false
|
1069
763
|
end
|
1070
764
|
end
|
1071
765
|
|
1072
766
|
private
|
1073
|
-
|
1074
|
-
def zoxide_available?
|
1075
|
-
system('which zoxide > /dev/null 2>&1')
|
1076
|
-
end
|
1077
|
-
|
1078
|
-
def get_zoxide_history
|
1079
|
-
return [] unless zoxide_available?
|
1080
|
-
|
1081
|
-
begin
|
1082
|
-
# zoxide query --list --score で履歴を取得(スコア順)
|
1083
|
-
output = `zoxide query --list --score 2>/dev/null`.strip
|
1084
|
-
return [] if output.empty?
|
1085
|
-
|
1086
|
-
# 各行をパスとスコアに分けて配列に変換
|
1087
|
-
lines = output.split("\n")
|
1088
|
-
history = lines.map do |line|
|
1089
|
-
# zoxide の出力は "スコア パス" の形式
|
1090
|
-
if line.match(/^\s*(\d+(?:\.\d+)?)\s+(.+)$/)
|
1091
|
-
score = $1.to_f
|
1092
|
-
path = $2.strip
|
1093
|
-
{ path: path, score: score }
|
1094
|
-
else
|
1095
|
-
# スコアがない場合はパスのみ(後方互換性)
|
1096
|
-
{ path: line.strip, score: 0.0 }
|
1097
|
-
end
|
1098
|
-
end
|
1099
|
-
|
1100
|
-
# 有効なディレクトリのみフィルタリング
|
1101
|
-
history.select { |entry| Dir.exist?(entry[:path]) }
|
1102
|
-
rescue StandardError
|
1103
|
-
[]
|
1104
|
-
end
|
1105
|
-
end
|
1106
|
-
|
1107
|
-
def show_no_zoxide_history_message
|
1108
|
-
title = 'Zoxide'
|
1109
|
-
content_lines = [
|
1110
|
-
'',
|
1111
|
-
'No zoxide history found.',
|
1112
|
-
'',
|
1113
|
-
'Zoxide learns from your directory navigation.',
|
1114
|
-
'Use zoxide more to build up history.',
|
1115
|
-
'',
|
1116
|
-
'Press any key to continue...'
|
1117
|
-
]
|
1118
|
-
|
1119
|
-
dialog_width = 45
|
1120
|
-
dialog_height = 4 + content_lines.length
|
1121
|
-
x, y = get_screen_center(dialog_width, dialog_height)
|
1122
|
-
|
1123
|
-
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
1124
|
-
border_color: "\e[33m", # 黄色
|
1125
|
-
title_color: "\e[1;33m", # 太字黄色
|
1126
|
-
content_color: "\e[37m" # 白色
|
1127
|
-
})
|
1128
|
-
|
1129
|
-
STDIN.getch
|
1130
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
1131
|
-
@terminal_ui&.refresh_display
|
1132
|
-
end
|
1133
|
-
|
1134
|
-
def select_from_zoxide_history(history)
|
1135
|
-
title = 'Zoxide History'
|
1136
|
-
|
1137
|
-
# 履歴を表示用に整形(最大20件)
|
1138
|
-
display_history = history.first(20)
|
1139
|
-
content_lines = ['']
|
1140
|
-
|
1141
|
-
display_history.each_with_index do |entry, index|
|
1142
|
-
# パスの表示を短縮(ホームディレクトリを ~ に置換)
|
1143
|
-
display_path = entry[:path].gsub(ENV['HOME'], '~')
|
1144
|
-
line = " #{index + 1}. #{display_path}"
|
1145
|
-
# 長すぎる場合は切り詰め
|
1146
|
-
line = line[0...60] + '...' if line.length > 63
|
1147
|
-
content_lines << line
|
1148
|
-
end
|
1149
|
-
|
1150
|
-
content_lines << ''
|
1151
|
-
content_lines << 'Enter number (1-' + display_history.length.to_s + ') or ESC to cancel'
|
1152
|
-
|
1153
|
-
dialog_width = 70
|
1154
|
-
dialog_height = [4 + content_lines.length, 25].min
|
1155
|
-
x, y = get_screen_center(dialog_width, dialog_height)
|
1156
|
-
|
1157
|
-
draw_floating_window(x, y, dialog_width, dialog_height, title, content_lines, {
|
1158
|
-
border_color: "\e[36m", # シアン色
|
1159
|
-
title_color: "\e[1;36m", # 太字シアン色
|
1160
|
-
content_color: "\e[37m" # 白色
|
1161
|
-
})
|
1162
|
-
|
1163
|
-
# 数字入力モード
|
1164
|
-
input_buffer = ''
|
1165
|
-
|
1166
|
-
loop do
|
1167
|
-
char = STDIN.getch
|
1168
|
-
|
1169
|
-
case char
|
1170
|
-
when "\e", "\x03" # ESC, Ctrl+C
|
1171
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
1172
|
-
@terminal_ui&.refresh_display
|
1173
|
-
return nil
|
1174
|
-
when "\r", "\n" # Enter
|
1175
|
-
if !input_buffer.empty?
|
1176
|
-
number = input_buffer.to_i
|
1177
|
-
if number > 0 && number <= display_history.length
|
1178
|
-
selected_entry = display_history[number - 1]
|
1179
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
1180
|
-
@terminal_ui&.refresh_display
|
1181
|
-
return selected_entry[:path]
|
1182
|
-
end
|
1183
|
-
end
|
1184
|
-
# 無効な入力の場合は再度入力を求める
|
1185
|
-
input_buffer = ''
|
1186
|
-
when "\u007f", "\b" # Backspace
|
1187
|
-
input_buffer = input_buffer[0...-1] unless input_buffer.empty?
|
1188
|
-
when /[0-9]/
|
1189
|
-
input_buffer += char
|
1190
|
-
# 最大2桁まで
|
1191
|
-
input_buffer = input_buffer[-2..-1] if input_buffer.length > 2
|
1192
|
-
|
1193
|
-
# 入力された数字が範囲内の場合は即座に選択
|
1194
|
-
number = input_buffer.to_i
|
1195
|
-
if number > 0 && number <= display_history.length &&
|
1196
|
-
(number >= 10 || input_buffer.length == 1)
|
1197
|
-
selected_entry = display_history[number - 1]
|
1198
|
-
clear_floating_window_area(x, y, dialog_width, dialog_height)
|
1199
|
-
@terminal_ui&.refresh_display
|
1200
|
-
return selected_entry[:path]
|
1201
|
-
end
|
1202
|
-
end
|
1203
|
-
end
|
1204
|
-
end
|
1205
|
-
|
1206
|
-
def navigate_to_zoxide_directory(target_path)
|
1207
|
-
return false unless Dir.exist?(target_path)
|
1208
|
-
|
1209
|
-
# DirectoryListingのnavigate_to_pathメソッドを使用してディレクトリに移動
|
1210
|
-
result = @directory_listing.navigate_to_path(target_path)
|
1211
|
-
if result
|
1212
|
-
@current_index = 0
|
1213
|
-
clear_filter_mode
|
1214
|
-
|
1215
|
-
# zoxide に移動を記録
|
1216
|
-
begin
|
1217
|
-
system("zoxide add #{Shellwords.escape(target_path)} > /dev/null 2>&1")
|
1218
|
-
rescue StandardError
|
1219
|
-
# zoxide add が失敗しても移動は成功として扱う
|
1220
|
-
end
|
1221
|
-
|
1222
|
-
true
|
1223
|
-
else
|
1224
|
-
false
|
1225
|
-
end
|
1226
|
-
end
|
1227
767
|
end
|
1228
768
|
end
|