beniya 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6ab1b0284a8b86ca20e3ba091dec0f285561c297418b922a436e66721a07e5b
4
- data.tar.gz: db0b99cb3adf8534dfa89d2110a02079bad81e04b77ef129884e0cd4aa242c5b
3
+ metadata.gz: 2e937b12eb5f1aa3425c8dd832cb7bd11396dfd86b6e232de58fe771c3da47a2
4
+ data.tar.gz: 1c41f33e6419d9458c778e8d7937472ad29e4f6d49ffcca12f1e78fac07f16ca
5
5
  SHA512:
6
- metadata.gz: a508d6b782f008de95b1c1bc46f0ab1b3ff371c806d277f4bdb7a493134ea0be3ffd678d5a63ea9916afed0256e05f17e08e62178ee1d84b0636e81554bb3c56
7
- data.tar.gz: 4474966812a065d193946935e65392eaa18265be92a3d4935655079ce54dc83c07325aa85b05753da0c0d202479e9db62595048028f6e4e9de978aebcef981be
6
+ metadata.gz: 272779c673aa653ffeada51b3f46ff4da876d2107310fd79460d1f3a50eae6690dd60d3c21fb9f9ff21ab6914e5baab7ac96c54cb273c68c0b4859d95cb66077
7
+ data.tar.gz: 4bf52e383efe9457ec8e0b46b90a5b0742eddd48a222197d83de6e374942cefb92df4c802cd40bb3b047c6e4180311e87a326fd1eabfb453f9b5bac3938d8dd6
data/README.md CHANGED
@@ -13,7 +13,9 @@ beniyaは、Yaziにインスパイアされたターミナル上で動作する
13
13
  - **軽量でシンプル**: Rubyで書かれた軽量なファイルマネージャー
14
14
  - **直感的な操作**: Vimライクなキーバインド
15
15
  - **ファイルプレビュー**: テキストファイルの内容をその場で確認
16
- - **リアルタイムフィルター**: Spaceキーでファイル名の絞り込み表示
16
+ - **ファイル選択・操作**: 複数ファイルの選択、移動、コピー、削除が可能
17
+ - **ベースディレクトリ操作**: 起動ディレクトリへの一括ファイル移動・コピー
18
+ - **リアルタイムフィルター**: sキーでファイル名の絞り込み表示
17
19
  - **高度な検索機能**: fzfとrgaを使った強力な検索
18
20
  - **マルチプラットフォーム**: macOS、Linux、Windowsで動作
19
21
  - **外部エディタ連携**: お気に入りのエディタでファイルを開く
@@ -77,6 +79,15 @@ beniya --help # ヘルプメッセージを表示
77
79
  | `a` | 新しいファイルを作成 |
78
80
  | `A` | 新しいディレクトリを作成 |
79
81
 
82
+ #### ファイル選択・操作
83
+
84
+ | キー | 機能 |
85
+ | ------- | ---------------------------------------------------- |
86
+ | `Space` | ファイル・ディレクトリを選択・選択解除(複数選択可) |
87
+ | `m` | 選択したアイテムをベースディレクトリに移動 |
88
+ | `p` | 選択したアイテムをベースディレクトリにコピー |
89
+ | `x` | 選択したアイテムを削除 |
90
+
80
91
  #### リアルタイムフィルター機能
81
92
 
82
93
  | キー | 機能 |
@@ -100,6 +111,44 @@ beniya --help # ヘルプメッセージを表示
100
111
  | ---- | ------------ |
101
112
  | `q` | beniyaを終了 |
102
113
 
114
+ ### ファイル選択・操作機能の詳細
115
+
116
+ #### ファイル・ディレクトリ選択 (`Space`)
117
+
118
+ - **選択・選択解除**: `Space`キーでファイルやディレクトリを選択・選択解除
119
+ - **複数選択**: 複数のファイルやディレクトリを同時に選択可能
120
+ - **視覚的表示**: 選択されたアイテムは✓マークと緑色の背景で表示
121
+ - **選択状態の表示**: 画面上部2行目に選択中のアイテム数が表示
122
+
123
+ #### ベースディレクトリ操作
124
+
125
+ beniya起動時のディレクトリが**ベースディレクトリ**として設定され、選択したファイルの移動・コピー先となります。
126
+
127
+ | 操作 | キー | 機能 |
128
+ | -------- | ---- | ---------------------------------------- |
129
+ | **移動** | `m` | 選択したアイテムをベースディレクトリに移動 |
130
+ | **コピー** | `p` | 選択したアイテムをベースディレクトリにコピー |
131
+ | **削除** | `x` | 選択したアイテムを削除 |
132
+
133
+ #### 操作の流れ
134
+
135
+ ```
136
+ 1. Space → ファイル・ディレクトリを選択(複数選択可)
137
+ 2. 操作キーを選択:
138
+ - m → ベースディレクトリに移動
139
+ - p → ベースディレクトリにコピー
140
+ - x → 削除
141
+ 3. y/n → 確認ダイアログで実行/キャンセル
142
+ 4. 結果表示 → 処理結果を確認
143
+ ```
144
+
145
+ #### 安全機能
146
+
147
+ - **確認ダイアログ**: 移動・コピー・削除前に必ず確認
148
+ - **重複チェック**: 同名ファイルがある場合は自動でスキップ
149
+ - **エラーハンドリング**: 権限不足などのエラーに適切に対応
150
+ - **操作ログ**: 処理結果を詳細に表示
151
+
103
152
  ### フィルター機能の詳細
104
153
 
105
154
  #### リアルタイムフィルター (`s`)
data/README_EN.md CHANGED
@@ -13,7 +13,9 @@ beniya is a terminal-based file manager inspired by Yazi. It's implemented in Ru
13
13
  - **Lightweight & Simple**: A lightweight file manager written in Ruby
14
14
  - **Intuitive Operation**: Vim-like key bindings
15
15
  - **File Preview**: View text file contents on the fly
16
- - **Real-time Filter**: Filter files by name using Space key
16
+ - **File Selection & Operations**: Select multiple files, move, copy, and delete
17
+ - **Base Directory Operations**: Batch file operations to startup directory
18
+ - **Real-time Filter**: Filter files by name using s key
17
19
  - **Advanced Search**: Powerful search using fzf and rga
18
20
  - **Multi-platform**: Runs on macOS, Linux, and Windows
19
21
  - **External Editor Integration**: Open files with your favorite editor
@@ -77,6 +79,15 @@ beniya --help # Show help message
77
79
  | `a` | Create new file |
78
80
  | `A` | Create new directory |
79
81
 
82
+ #### File Selection & Operations
83
+
84
+ | Key | Function |
85
+ | ------- | ------------------------------------------- |
86
+ | `Space` | Select/deselect files and directories |
87
+ | `m` | Move selected items to base directory |
88
+ | `p` | Copy selected items to base directory |
89
+ | `x` | Delete selected items |
90
+
80
91
  #### Real-time Filter
81
92
 
82
93
  | Key | Function |
@@ -100,6 +111,44 @@ beniya --help # Show help message
100
111
  | --- | ----------- |
101
112
  | `q` | Quit beniya |
102
113
 
114
+ ### File Selection & Operations
115
+
116
+ #### File and Directory Selection (`Space`)
117
+
118
+ - **Select/Deselect**: Use `Space` key to select or deselect files and directories
119
+ - **Multiple Selection**: Select multiple files and directories simultaneously
120
+ - **Visual Display**: Selected items are marked with ✓ and highlighted in green
121
+ - **Selection Counter**: The number of selected items is shown on the second line of the screen
122
+
123
+ #### Base Directory Operations
124
+
125
+ The directory where beniya starts becomes the **base directory**, which serves as the destination for move and copy operations.
126
+
127
+ | Operation | Key | Function |
128
+ | --------- | --- | --------------------------------------- |
129
+ | **Move** | `m` | Move selected items to base directory |
130
+ | **Copy** | `p` | Copy selected items to base directory |
131
+ | **Delete** | `x` | Delete selected items |
132
+
133
+ #### Operation Workflow
134
+
135
+ ```
136
+ 1. Space → Select files/directories (multiple selection possible)
137
+ 2. Choose operation key:
138
+ - m → Move to base directory
139
+ - p → Copy to base directory
140
+ - x → Delete
141
+ 3. y/n → Confirm or cancel in confirmation dialog
142
+ 4. Result Display → Review operation results
143
+ ```
144
+
145
+ #### Safety Features
146
+
147
+ - **Confirmation Dialog**: Always confirm before move/copy/delete operations
148
+ - **Duplicate Check**: Automatically skip files with same names
149
+ - **Error Handling**: Proper handling of permission errors and other issues
150
+ - **Operation Log**: Detailed display of operation results
151
+
103
152
  ### Filter Feature
104
153
 
105
154
  #### Real-time Filter (`s`)
@@ -12,6 +12,7 @@ module Beniya
12
12
  # 各コンポーネントを初期化
13
13
  directory_listing = DirectoryListing.new(@start_directory)
14
14
  keybind_handler = KeybindHandler.new
15
+ keybind_handler.set_base_directory(@start_directory)
15
16
  file_preview = FilePreview.new
16
17
  terminal_ui = TerminalUI.new
17
18
 
@@ -19,8 +19,8 @@ module Beniya
19
19
 
20
20
  begin
21
21
  # binary file detection
22
- sample = File.read(file_path, [file_size, 512].min)
23
- return binary_response if binary_file?(sample)
22
+ sample = File.binread(file_path, [file_size, 512].min)
23
+ return binary_response(file_path) if binary_file?(sample)
24
24
 
25
25
  # process as text file
26
26
  lines = read_text_file(file_path, max_lines)
@@ -124,6 +124,14 @@ module Beniya
124
124
  { type: "code", language: "markdown" }
125
125
  when ".txt", ".log"
126
126
  { type: "text", language: nil }
127
+ when ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z"
128
+ { type: "archive", language: nil }
129
+ when ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg"
130
+ { type: "image", language: nil }
131
+ when ".pdf"
132
+ { type: "document", language: nil }
133
+ when ".exe", ".dmg", ".deb", ".rpm"
134
+ { type: "executable", language: nil }
127
135
  else
128
136
  { type: "text", language: nil }
129
137
  end
@@ -147,7 +155,19 @@ module Beniya
147
155
  }
148
156
  end
149
157
 
150
- def binary_response
158
+ def binary_response(file_path = nil)
159
+ file_size = file_path ? File.size(file_path) : 0
160
+ modified_time = file_path ? File.mtime(file_path) : Time.now
161
+
162
+ {
163
+ type: "binary",
164
+ message: "#{ConfigLoader.message('file.binary_file')} - #{ConfigLoader.message('file.cannot_preview')}",
165
+ lines: ["(#{ConfigLoader.message('file.binary_file')})"],
166
+ size: file_size,
167
+ modified: modified_time,
168
+ encoding: "binary"
169
+ }
170
+ rescue => e
151
171
  {
152
172
  type: "binary",
153
173
  message: "#{ConfigLoader.message('file.binary_file')} - #{ConfigLoader.message('file.cannot_preview')}",
@@ -13,6 +13,8 @@ module Beniya
13
13
  @filter_query = ""
14
14
  @filtered_entries = []
15
15
  @original_entries = []
16
+ @selected_items = []
17
+ @base_directory = nil
16
18
  end
17
19
 
18
20
  def set_directory_listing(directory_listing)
@@ -24,6 +26,18 @@ module Beniya
24
26
  @terminal_ui = terminal_ui
25
27
  end
26
28
 
29
+ def set_base_directory(base_dir)
30
+ @base_directory = File.expand_path(base_dir)
31
+ end
32
+
33
+ def selected_items
34
+ @selected_items.dup
35
+ end
36
+
37
+ def is_selected?(entry_name)
38
+ @selected_items.include?(entry_name)
39
+ end
40
+
27
41
  def handle_key(key)
28
42
  return false unless @directory_listing
29
43
 
@@ -60,8 +74,8 @@ module Beniya
60
74
  # 新規フィルターモード開始
61
75
  start_filter_mode
62
76
  end
63
- when ' ' # Space - disabled for future functionality
64
- false
77
+ when ' ' # Space - toggle selection
78
+ toggle_selection
65
79
  when "\e" # ESC
66
80
  if !@filter_query.empty?
67
81
  # フィルタが設定されている場合はクリア
@@ -82,6 +96,12 @@ module Beniya
82
96
  create_file
83
97
  when 'A' # A
84
98
  create_directory
99
+ when 'm' # m - move selected files to base directory
100
+ move_selected_to_base
101
+ when 'p' # p - copy selected files to base directory
102
+ copy_selected_to_base
103
+ when 'x' # x - delete selected files
104
+ delete_selected_files
85
105
  else
86
106
  false # #{ConfigLoader.message('keybind.invalid_key')}
87
107
  end
@@ -435,5 +455,132 @@ module Beniya
435
455
  false
436
456
  end
437
457
  end
458
+
459
+ def toggle_selection
460
+ entry = current_entry
461
+ return false unless entry
462
+
463
+ if @selected_items.include?(entry[:name])
464
+ @selected_items.delete(entry[:name])
465
+ else
466
+ @selected_items << entry[:name]
467
+ end
468
+ true
469
+ end
470
+
471
+ def move_selected_to_base
472
+ return false if @selected_items.empty? || @base_directory.nil?
473
+
474
+ if show_confirmation_dialog("移動", @selected_items.length)
475
+ perform_file_operation(:move, @selected_items, @base_directory)
476
+ else
477
+ false
478
+ end
479
+ end
480
+
481
+ def copy_selected_to_base
482
+ return false if @selected_items.empty? || @base_directory.nil?
483
+
484
+ if show_confirmation_dialog("コピー", @selected_items.length)
485
+ perform_file_operation(:copy, @selected_items, @base_directory)
486
+ else
487
+ false
488
+ end
489
+ end
490
+
491
+ def show_confirmation_dialog(operation, count)
492
+ print "\n#{count}個のアイテムを#{operation}しますか? (y/n): "
493
+ response = STDIN.gets.chomp.downcase
494
+ response == 'y' || response == 'yes'
495
+ end
496
+
497
+ def perform_file_operation(operation, items, destination)
498
+ success_count = 0
499
+ current_path = @directory_listing&.current_path || Dir.pwd
500
+
501
+ items.each do |item_name|
502
+ source_path = File.join(current_path, item_name)
503
+ dest_path = File.join(destination, item_name)
504
+
505
+ begin
506
+ case operation
507
+ when :move
508
+ if File.exist?(dest_path)
509
+ puts "\n#{item_name} は既に移動先に存在します。スキップします。"
510
+ next
511
+ end
512
+ FileUtils.mv(source_path, dest_path)
513
+ when :copy
514
+ if File.exist?(dest_path)
515
+ puts "\n#{item_name} は既に移動先に存在します。スキップします。"
516
+ next
517
+ end
518
+ if File.directory?(source_path)
519
+ FileUtils.cp_r(source_path, dest_path)
520
+ else
521
+ FileUtils.cp(source_path, dest_path)
522
+ end
523
+ end
524
+ success_count += 1
525
+ rescue => e
526
+ puts "\n#{item_name} の#{operation == :move ? '移動' : 'コピー'}に失敗: #{e.message}"
527
+ end
528
+ end
529
+
530
+ # 操作完了後の処理
531
+ @selected_items.clear
532
+ @directory_listing.refresh if @directory_listing
533
+
534
+ puts "\n#{success_count}個のアイテムを#{operation == :move ? '移動' : 'コピー'}しました。"
535
+ print "何かキーを押してください..."
536
+ STDIN.getch
537
+ true
538
+ end
539
+
540
+ def delete_selected_files
541
+ return false if @selected_items.empty?
542
+
543
+ if show_delete_confirmation(@selected_items.length)
544
+ perform_delete_operation(@selected_items)
545
+ else
546
+ false
547
+ end
548
+ end
549
+
550
+ def show_delete_confirmation(count)
551
+ print "\n#{count}個のアイテムを削除しますか? (y/n): "
552
+ response = STDIN.gets.chomp.downcase
553
+ response == 'y' || response == 'yes'
554
+ end
555
+
556
+ def perform_delete_operation(items)
557
+ success_count = 0
558
+ current_path = @directory_listing&.current_path || Dir.pwd
559
+
560
+ items.each do |item_name|
561
+ item_path = File.join(current_path, item_name)
562
+
563
+ begin
564
+ if File.directory?(item_path)
565
+ FileUtils.rm_rf(item_path)
566
+ else
567
+ FileUtils.rm(item_path)
568
+ end
569
+ success_count += 1
570
+ puts "\n#{item_name} を削除しました。"
571
+ rescue => e
572
+ puts "\n#{item_name} の削除に失敗: #{e.message}"
573
+ end
574
+ end
575
+
576
+ # 削除完了後の処理
577
+ @selected_items.clear
578
+ @directory_listing.refresh if @directory_listing
579
+
580
+ puts "\n#{success_count}個のアイテムを削除しました。"
581
+ print "何かキーを押してください..."
582
+ STDIN.getch
583
+ true
584
+ end
438
585
  end
439
- end
586
+ end
@@ -75,15 +75,16 @@ module Beniya
75
75
  # move cursor to top of screen (don't clear)
76
76
  print "\e[H"
77
77
 
78
- # header
78
+ # header (2 lines)
79
79
  draw_header
80
+ draw_base_directory_info
80
81
 
81
82
  # main content (left: directory list, right: preview)
82
83
  entries = get_display_entries
83
84
  selected_entry = entries[@keybind_handler.current_index]
84
85
 
85
- # calculate height with footer margin
86
- content_height = @screen_height - 3 # ヘッダーとフッター分を除く
86
+ # calculate height with header (2 lines) and footer margin
87
+ content_height = @screen_height - 4 # ヘッダー(2行)とフッター分を除く
87
88
  left_width = @screen_width / 2
88
89
  right_width = @screen_width - left_width
89
90
 
@@ -125,13 +126,50 @@ module Beniya
125
126
  puts "\e[7m#{header.ljust(@screen_width)}\e[0m" # reverse display
126
127
  end
127
128
 
129
+ def draw_base_directory_info
130
+ # 強制的に表示 - デバッグ用に安全チェックを緩和
131
+ if @keybind_handler && @keybind_handler.instance_variable_get(:@base_directory)
132
+ base_dir = @keybind_handler.instance_variable_get(:@base_directory)
133
+ selected_count = @keybind_handler.selected_items.length
134
+ base_info = "📋 ベースディレクトリ: #{base_dir}"
135
+
136
+ # 選択されたアイテム数を表示
137
+ if selected_count > 0
138
+ base_info += " | 選択中: #{selected_count}個"
139
+ end
140
+ else
141
+ # keybind_handlerがない場合、またはbase_directoryが設定されていない場合
142
+ base_info = "📋 ベースディレクトリ: #{Dir.pwd}"
143
+ end
144
+
145
+ # 長すぎる場合は省略
146
+ if base_info.length > @screen_width - 2
147
+ if base_info.include?(" | 選択中:")
148
+ selected_part = base_info.split(" | 選択中:").last
149
+ available_length = @screen_width - 20 - " | 選択中:#{selected_part}".length
150
+ else
151
+ available_length = @screen_width - 20
152
+ end
153
+
154
+ if available_length > 10
155
+ # パスの最後の部分を表示
156
+ dir_part = base_info.split(": ").last.split(" | ").first
157
+ short_base_dir = "...#{dir_part[-available_length..-1]}"
158
+ base_info = base_info.gsub(dir_part, short_base_dir)
159
+ end
160
+ end
161
+
162
+ # 2行目に確実に表示
163
+ print "\e[2;1H\e[44m\e[37m#{base_info.ljust(@screen_width)}\e[0m"
164
+ end
165
+
128
166
  def draw_directory_list(entries, width, height)
129
167
  start_index = [@keybind_handler.current_index - height / 2, 0].max
130
168
  [start_index + height - 1, entries.length - 1].min
131
169
 
132
170
  (0...height).each do |i|
133
171
  entry_index = start_index + i
134
- line_num = i + 2 # skip header
172
+ line_num = i + 3 # skip header (2 lines)
135
173
 
136
174
  print "\e[#{line_num};1H" # set cursor position
137
175
 
@@ -155,16 +193,19 @@ module Beniya
155
193
  # 左ペイン専用の安全な幅を計算(右ペインにはみ出さないよう)
156
194
  safe_width = [width - 1, @screen_width / 2 - 1].min
157
195
 
196
+ # 選択マークの追加
197
+ selection_mark = @keybind_handler.is_selected?(entry[:name]) ? "✓ " : " "
198
+
158
199
  # ファイル名(必要に応じて切り詰め)
159
200
  name = entry[:name]
160
- max_name_length = safe_width - 10 # アイコンとサイズ情報分を除く
201
+ max_name_length = safe_width - 12 # アイコン、選択マーク、サイズ情報分を除く
161
202
  name = name[0...max_name_length - 3] + '...' if max_name_length > 0 && name.length > max_name_length
162
203
 
163
204
  # サイズ情報
164
205
  size_info = format_size(entry[:size])
165
206
 
166
207
  # 行の内容を構築(安全な幅内で)
167
- content_without_size = "#{icon} #{name}"
208
+ content_without_size = "#{selection_mark}#{icon} #{name}"
168
209
  available_for_content = safe_width - size_info.length
169
210
 
170
211
  line_content = if available_for_content > 0
@@ -180,7 +221,12 @@ module Beniya
180
221
  selected_color = ColorHelper.color_to_selected_ansi(ConfigLoader.colors[:selected])
181
222
  print "#{selected_color}#{line_content}#{ColorHelper.reset}"
182
223
  else
183
- print "#{color}#{line_content}#{ColorHelper.reset}"
224
+ # 選択されたアイテムは異なる色で表示
225
+ if @keybind_handler.is_selected?(entry[:name])
226
+ print "\e[42m\e[30m#{line_content}\e[0m" # 緑背景、黒文字
227
+ else
228
+ print "#{color}#{line_content}#{ColorHelper.reset}"
229
+ end
184
230
  end
185
231
  end
186
232
 
@@ -226,7 +272,7 @@ module Beniya
226
272
 
227
273
  def draw_file_preview(selected_entry, width, height, left_offset)
228
274
  (0...height).each do |i|
229
- line_num = i + 2
275
+ line_num = i + 3 # skip header (2 lines)
230
276
  # カーソル位置を左パネルの右端に設定
231
277
  cursor_position = left_offset + 1
232
278
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Beniya
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beniya
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-26 00:00:00.000000000 Z
10
+ date: 2025-09-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: io-console