beniya 0.1.1 → 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: 11ed7079e0eaea122bc96a04729adcab2490b3f111472433b5a13d08f2fc1d9c
4
- data.tar.gz: 4fa0fd0feab300b75ba97b1500b6d80492525cd814bd0a7a402900721746d4df
3
+ metadata.gz: 2e937b12eb5f1aa3425c8dd832cb7bd11396dfd86b6e232de58fe771c3da47a2
4
+ data.tar.gz: 1c41f33e6419d9458c778e8d7937472ad29e4f6d49ffcca12f1e78fac07f16ca
5
5
  SHA512:
6
- metadata.gz: ecf5c4c846d7bd17d619bdbd545646d89d9322373f1ba94e024d75d37c7703711aa66179b61b85e4941b538dd967d1d252c8a90f2bfbc063d878f923f907b8b4
7
- data.tar.gz: 4d8ea0a3d7a815b4f83119a847c562dc9d87cf29de40cee238a8e394a4fde2e83588db849fc8b843c31108f2664131a15dd3f550d5ce438337f97326d5c989aa
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
  - **外部エディタ連携**: お気に入りのエディタでファイルを開く
@@ -74,12 +76,23 @@ beniya --help # ヘルプメッセージを表示
74
76
  | `o` | 選択中のファイルを外部エディタで開く |
75
77
  | `e` | 現在のディレクトリをファイルエクスプローラーで開く |
76
78
  | `r` | ディレクトリ内容を更新 |
79
+ | `a` | 新しいファイルを作成 |
80
+ | `A` | 新しいディレクトリを作成 |
81
+
82
+ #### ファイル選択・操作
83
+
84
+ | キー | 機能 |
85
+ | ------- | ---------------------------------------------------- |
86
+ | `Space` | ファイル・ディレクトリを選択・選択解除(複数選択可) |
87
+ | `m` | 選択したアイテムをベースディレクトリに移動 |
88
+ | `p` | 選択したアイテムをベースディレクトリにコピー |
89
+ | `x` | 選択したアイテムを削除 |
77
90
 
78
91
  #### リアルタイムフィルター機能
79
92
 
80
93
  | キー | 機能 |
81
94
  | ----------- | ------------------------------------------ |
82
- | `Space` | フィルターモード開始・再編集 |
95
+ | `s` | フィルターモード開始・再編集 |
83
96
  | 文字入力 | ファイル名で絞り込み(フィルターモード中) |
84
97
  | `Enter` | フィルター維持で通常モードに戻る |
85
98
  | `ESC` | フィルターをクリアして通常モードに戻る |
@@ -89,8 +102,8 @@ beniya --help # ヘルプメッセージを表示
89
102
 
90
103
  | キー | 機能 |
91
104
  | ---- | ----------------------------------------- |
92
- | `/` | fzfによるファイル名検索(プレビュー付き) |
93
- | `f` | rgaによるファイル内容検索 |
105
+ | `f` | fzfによるファイル名検索(プレビュー付き) |
106
+ | `F` | rgaによるファイル内容検索 |
94
107
 
95
108
  #### システム操作
96
109
 
@@ -98,38 +111,76 @@ beniya --help # ヘルプメッセージを表示
98
111
  | ---- | ------------ |
99
112
  | `q` | beniyaを終了 |
100
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
+
101
152
  ### フィルター機能の詳細
102
153
 
103
- #### リアルタイムフィルター (`Space`)
154
+ #### リアルタイムフィルター (`s`)
104
155
 
105
- - **フィルター開始**: `Space`キーを押してフィルターモードに入る
156
+ - **フィルター開始**: `s`キーを押してフィルターモードに入る
106
157
  - **文字入力による絞り込み**: 日本語・英数字・記号に対応
107
158
  - **リアルタイム更新**: 文字を入力するたびに表示が更新される
108
159
  - **フィルター維持**: `Enter`キーでフィルターを保持したまま通常操作に戻る
109
160
  - **フィルタークリア**: `ESC`キーでフィルターをクリアして通常表示に戻る
110
- - **再編集**: フィルター適用中に`Space`キーで再編集モード
161
+ - **再編集**: フィルター適用中に`s`キーで再編集モード
111
162
  - **文字削除**: `Backspace`で文字削除、全削除時は自動でフィルタークリア
112
163
 
113
164
  #### 使用例
114
165
 
115
166
  ```
116
- 1. Space → フィルターモード開始
167
+ 1. s → フィルターモード開始
117
168
  2. ".rb" → Rubyファイルのみ表示
118
169
  3. Enter → フィルター維持で通常操作
119
170
  4. j/k → フィルター結果内をナビゲーション
120
- 5. Space → フィルター再編集
171
+ 5. s → フィルター再編集
121
172
  6. ESC → フィルタークリア
122
173
  ```
123
174
 
124
175
  ### 検索機能の詳細
125
176
 
126
- #### ファイル名検索 (`/`)
177
+ #### ファイル名検索 (`f`)
127
178
 
128
179
  - `fzf`を使用したインタラクティブなファイル名検索
129
180
  - リアルタイムプレビュー表示
130
181
  - 選択したファイルは自動で外部エディタで開かれる
131
182
 
132
- #### ファイル内容検索 (`f`)
183
+ #### ファイル内容検索 (`F`)
133
184
 
134
185
  - `rga`(ripgrep-all)を使用した高度なファイル内容検索
135
186
  - PDF、Word文書、画像内テキストなども検索対象
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
@@ -74,12 +76,23 @@ beniya --help # Show help message
74
76
  | `o` | Open selected file with external editor |
75
77
  | `e` | Open current directory in file explorer |
76
78
  | `r` | Refresh directory contents |
79
+ | `a` | Create new file |
80
+ | `A` | Create new directory |
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 |
77
90
 
78
91
  #### Real-time Filter
79
92
 
80
93
  | Key | Function |
81
94
  | ----------- | -------------------------------------- |
82
- | `Space` | Start filter mode / Re-edit filter |
95
+ | `s` | Start filter mode / Re-edit filter |
83
96
  | Text input | Filter files by name (in filter mode) |
84
97
  | `Enter` | Keep filter and return to normal mode |
85
98
  | `ESC` | Clear filter and return to normal mode |
@@ -89,8 +102,8 @@ beniya --help # Show help message
89
102
 
90
103
  | Key | Function |
91
104
  | --- | ---------------------------------------- |
92
- | `/` | File name search with fzf (with preview) |
93
- | `f` | File content search with rga |
105
+ | `f` | File name search with fzf (with preview) |
106
+ | `F` | File content search with rga |
94
107
 
95
108
  #### System Operations
96
109
 
@@ -98,38 +111,76 @@ beniya --help # Show help message
98
111
  | --- | ----------- |
99
112
  | `q` | Quit beniya |
100
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
+
101
152
  ### Filter Feature
102
153
 
103
- #### Real-time Filter (`Space`)
154
+ #### Real-time Filter (`s`)
104
155
 
105
- - **Start Filter**: Press `Space` to enter filter mode
156
+ - **Start Filter**: Press `s` to enter filter mode
106
157
  - **Text Input Filtering**: Supports Japanese, English, numbers, and symbols
107
158
  - **Real-time Updates**: Display updates with each character typed
108
159
  - **Keep Filter**: Press `Enter` to maintain filter while returning to normal operations
109
160
  - **Clear Filter**: Press `ESC` to clear filter and return to normal display
110
- - **Re-edit**: Press `Space` again while filter is active to re-edit
161
+ - **Re-edit**: Press `s` again while filter is active to re-edit
111
162
  - **Character Deletion**: Use `Backspace` to delete characters, auto-clear when empty
112
163
 
113
164
  #### Usage Example
114
165
 
115
166
  ```
116
- 1. Space → Start filter mode
167
+ 1. s → Start filter mode
117
168
  2. ".rb" → Show only Ruby files
118
169
  3. Enter → Keep filter, return to normal operations
119
170
  4. j/k → Navigate within filtered results
120
- 5. Space → Re-edit filter
171
+ 5. s → Re-edit filter
121
172
  6. ESC → Clear filter
122
173
  ```
123
174
 
124
175
  ### Search Features
125
176
 
126
- #### File Name Search (`/`)
177
+ #### File Name Search (`f`)
127
178
 
128
179
  - Interactive file name search using `fzf`
129
180
  - Real-time preview display
130
181
  - Selected files automatically open in external editor
131
182
 
132
- #### File Content Search (`f`)
183
+ #### File Content Search (`F`)
133
184
 
134
185
  - Advanced file content search using `rga` (ripgrep-all)
135
186
  - Searches PDFs, Word documents, text in images, and more
@@ -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
 
data/lib/beniya/config.rb CHANGED
@@ -29,6 +29,15 @@ module Beniya
29
29
  'keybind.search_text' => 'Search text: ',
30
30
  'keybind.no_matches' => 'No matches found.',
31
31
  'keybind.press_any_key' => 'Press any key to continue...',
32
+ 'keybind.input_filename' => 'Enter filename: ',
33
+ 'keybind.input_dirname' => 'Enter directory name: ',
34
+ 'keybind.invalid_filename' => 'Invalid filename (cannot contain / or \\)',
35
+ 'keybind.invalid_dirname' => 'Invalid directory name (cannot contain / or \\)',
36
+ 'keybind.file_exists' => 'File already exists',
37
+ 'keybind.directory_exists' => 'Directory already exists',
38
+ 'keybind.file_created' => 'File created',
39
+ 'keybind.directory_created' => 'Directory created',
40
+ 'keybind.creation_error' => 'Creation error',
32
41
 
33
42
  # UI messages
34
43
  'ui.operation_prompt' => 'Operation: ',
@@ -88,6 +97,15 @@ module Beniya
88
97
  'keybind.search_text' => '検索テキスト: ',
89
98
  'keybind.no_matches' => 'マッチするものが見つかりません。',
90
99
  'keybind.press_any_key' => '何かキーを押して続行...',
100
+ 'keybind.input_filename' => 'ファイル名を入力: ',
101
+ 'keybind.input_dirname' => 'ディレクトリ名を入力: ',
102
+ 'keybind.invalid_filename' => '無効なファイル名(/や\\を含むことはできません)',
103
+ 'keybind.invalid_dirname' => '無効なディレクトリ名(/や\\を含むことはできません)',
104
+ 'keybind.file_exists' => 'ファイルが既に存在します',
105
+ 'keybind.directory_exists' => 'ディレクトリが既に存在します',
106
+ 'keybind.file_created' => 'ファイルを作成しました',
107
+ 'keybind.directory_created' => 'ディレクトリを作成しました',
108
+ 'keybind.creation_error' => '作成エラー',
91
109
 
92
110
  # UI messages
93
111
  'ui.operation_prompt' => '操作: ',
@@ -22,6 +22,13 @@ module Beniya
22
22
  execute_command_with_line(application, file_path, line_number)
23
23
  end
24
24
 
25
+ def open_directory_in_explorer(directory_path)
26
+ return false unless File.exist?(directory_path)
27
+ return false unless File.directory?(directory_path)
28
+
29
+ execute_explorer_command(directory_path)
30
+ end
31
+
25
32
  private
26
33
 
27
34
  def find_application_for_file(file_path)
@@ -109,6 +116,25 @@ module Beniya
109
116
  argument
110
117
  end
111
118
  end
119
+
120
+ def execute_explorer_command(directory_path)
121
+ quoted_path = quote_shell_argument(directory_path)
122
+
123
+ case RbConfig::CONFIG['host_os']
124
+ when /mswin|mingw|cygwin/
125
+ # Windows
126
+ system("explorer #{quoted_path}")
127
+ when /darwin/
128
+ # macOS
129
+ system("open #{quoted_path}")
130
+ else
131
+ # Linux/Unix
132
+ system("xdg-open #{quoted_path}")
133
+ end
134
+ rescue StandardError => e
135
+ warn "ディレクトリを開けませんでした: #{e.message}"
136
+ false
137
+ end
112
138
  end
113
139
  end
114
140
 
@@ -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')}",
@@ -7,11 +7,14 @@ module Beniya
7
7
  def initialize
8
8
  @current_index = 0
9
9
  @directory_listing = nil
10
+ @terminal_ui = nil
10
11
  @file_opener = FileOpener.new
11
12
  @filter_mode = false
12
13
  @filter_query = ""
13
14
  @filtered_entries = []
14
15
  @original_entries = []
16
+ @selected_items = []
17
+ @base_directory = nil
15
18
  end
16
19
 
17
20
  def set_directory_listing(directory_listing)
@@ -19,6 +22,22 @@ module Beniya
19
22
  @current_index = 0
20
23
  end
21
24
 
25
+ def set_terminal_ui(terminal_ui)
26
+ @terminal_ui = terminal_ui
27
+ end
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
+
22
41
  def handle_key(key)
23
42
  return false unless @directory_listing
24
43
 
@@ -44,7 +63,9 @@ module Beniya
44
63
  refresh
45
64
  when 'o' # o
46
65
  open_current_file
47
- when ' ' # Space
66
+ when 'e' # e - open directory in file explorer
67
+ open_directory_in_explorer
68
+ when 's' # s - filter files
48
69
  if !@filter_query.empty?
49
70
  # フィルタが設定されている場合は再編集モードに入る
50
71
  @filter_mode = true
@@ -53,6 +74,8 @@ module Beniya
53
74
  # 新規フィルターモード開始
54
75
  start_filter_mode
55
76
  end
77
+ when ' ' # Space - toggle selection
78
+ toggle_selection
56
79
  when "\e" # ESC
57
80
  if !@filter_query.empty?
58
81
  # フィルタが設定されている場合はクリア
@@ -63,12 +86,22 @@ module Beniya
63
86
  end
64
87
  when 'q' # q
65
88
  exit_request
66
- when 'e' # e
67
- open_explorer
68
89
  when '/' # /
69
90
  fzf_search
70
- when 'f' # f
91
+ when 'f' # f - file name search with fzf
92
+ fzf_search
93
+ when 'F' # F - file content search with rga
71
94
  rga_search
95
+ when 'a' # a
96
+ create_file
97
+ when 'A' # A
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
72
105
  else
73
106
  false # #{ConfigLoader.message('keybind.invalid_key')}
74
107
  end
@@ -151,6 +184,9 @@ module Beniya
151
184
  end
152
185
 
153
186
  def refresh
187
+ # ウィンドウサイズを更新して画面を再描画
188
+ @terminal_ui&.refresh_display
189
+
154
190
  @directory_listing.refresh
155
191
  if @filter_mode || !@filter_query.empty?
156
192
  # Re-apply filter with new directory contents
@@ -176,25 +212,16 @@ module Beniya
176
212
  end
177
213
  end
178
214
 
179
- def exit_request
180
- true # request exit
181
- end
182
-
183
- def open_explorer
215
+ def open_directory_in_explorer
184
216
  current_path = @directory_listing&.current_path || Dir.pwd
185
-
186
- case RUBY_PLATFORM
187
- when /darwin/ # macOS
188
- system("open", current_path)
189
- when /linux/ # Linux
190
- system("xdg-open", current_path)
191
- when /mswin|mingw|cygwin/ # Windows
192
- system("explorer", current_path)
193
- end
194
-
217
+ @file_opener.open_directory_in_explorer(current_path)
195
218
  true
196
219
  end
197
220
 
221
+ def exit_request
222
+ true # request exit
223
+ end
224
+
198
225
  def fzf_search
199
226
  return false unless fzf_available?
200
227
 
@@ -328,5 +355,232 @@ module Beniya
328
355
  # 既存メソッド(後方互換用)
329
356
  clear_filter_mode
330
357
  end
358
+
359
+ def create_file
360
+ current_path = @directory_listing&.current_path || Dir.pwd
361
+
362
+ # ファイル名の入力を求める
363
+ print ConfigLoader.message('keybind.input_filename')
364
+ filename = STDIN.gets.chomp
365
+ return false if filename.empty?
366
+
367
+ # 不正なファイル名のチェック
368
+ if filename.include?('/') || filename.include?('\\')
369
+ puts "\n#{ConfigLoader.message('keybind.invalid_filename')}"
370
+ print ConfigLoader.message('keybind.press_any_key')
371
+ STDIN.getch
372
+ return false
373
+ end
374
+
375
+ file_path = File.join(current_path, filename)
376
+
377
+ # ファイルが既に存在する場合の確認
378
+ if File.exist?(file_path)
379
+ puts "\n#{ConfigLoader.message('keybind.file_exists')}"
380
+ print ConfigLoader.message('keybind.press_any_key')
381
+ STDIN.getch
382
+ return false
383
+ end
384
+
385
+ begin
386
+ # ファイルを作成
387
+ File.write(file_path, '')
388
+
389
+ # ディレクトリ表示を更新
390
+ @directory_listing.refresh
391
+
392
+ # 作成したファイルを選択状態にする
393
+ entries = @directory_listing.list_entries
394
+ new_file_index = entries.find_index { |entry| entry[:name] == filename }
395
+ @current_index = new_file_index if new_file_index
396
+
397
+ puts "\n#{ConfigLoader.message('keybind.file_created')}: #{filename}"
398
+ print ConfigLoader.message('keybind.press_any_key')
399
+ STDIN.getch
400
+ true
401
+ rescue => e
402
+ puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
403
+ print ConfigLoader.message('keybind.press_any_key')
404
+ STDIN.getch
405
+ false
406
+ end
407
+ end
408
+
409
+ def create_directory
410
+ current_path = @directory_listing&.current_path || Dir.pwd
411
+
412
+ # ディレクトリ名の入力を求める
413
+ print ConfigLoader.message('keybind.input_dirname')
414
+ dirname = STDIN.gets.chomp
415
+ return false if dirname.empty?
416
+
417
+ # 不正なディレクトリ名のチェック
418
+ if dirname.include?('/') || dirname.include?('\\')
419
+ puts "\n#{ConfigLoader.message('keybind.invalid_dirname')}"
420
+ print ConfigLoader.message('keybind.press_any_key')
421
+ STDIN.getch
422
+ return false
423
+ end
424
+
425
+ dir_path = File.join(current_path, dirname)
426
+
427
+ # ディレクトリが既に存在する場合の確認
428
+ if File.exist?(dir_path)
429
+ puts "\n#{ConfigLoader.message('keybind.directory_exists')}"
430
+ print ConfigLoader.message('keybind.press_any_key')
431
+ STDIN.getch
432
+ return false
433
+ end
434
+
435
+ begin
436
+ # ディレクトリを作成
437
+ Dir.mkdir(dir_path)
438
+
439
+ # ディレクトリ表示を更新
440
+ @directory_listing.refresh
441
+
442
+ # 作成したディレクトリを選択状態にする
443
+ entries = @directory_listing.list_entries
444
+ new_dir_index = entries.find_index { |entry| entry[:name] == dirname }
445
+ @current_index = new_dir_index if new_dir_index
446
+
447
+ puts "\n#{ConfigLoader.message('keybind.directory_created')}: #{dirname}"
448
+ print ConfigLoader.message('keybind.press_any_key')
449
+ STDIN.getch
450
+ true
451
+ rescue => e
452
+ puts "\n#{ConfigLoader.message('keybind.creation_error')}: #{e.message}"
453
+ print ConfigLoader.message('keybind.press_any_key')
454
+ STDIN.getch
455
+ false
456
+ end
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
331
585
  end
332
- end
586
+ end
@@ -21,6 +21,7 @@ module Beniya
21
21
  @keybind_handler = keybind_handler
22
22
  @file_preview = file_preview
23
23
  @keybind_handler.set_directory_listing(@directory_listing)
24
+ @keybind_handler.set_terminal_ui(self)
24
25
 
25
26
  @running = true
26
27
  setup_terminal
@@ -32,6 +33,12 @@ module Beniya
32
33
  end
33
34
  end
34
35
 
36
+ def refresh_display
37
+ # ウィンドウサイズを更新してから画面をクリアして再描画
38
+ update_screen_size
39
+ print "\e[2J\e[H" # clear screen, cursor to home
40
+ end
41
+
35
42
  private
36
43
 
37
44
  def setup_terminal
@@ -41,6 +48,10 @@ module Beniya
41
48
  print "\e[2J\e[H" # clear screen, cursor to home (first time only)
42
49
 
43
50
  # re-acquire terminal size (just in case)
51
+ update_screen_size
52
+ end
53
+
54
+ def update_screen_size
44
55
  console = IO.console
45
56
  return unless console
46
57
 
@@ -64,15 +75,16 @@ module Beniya
64
75
  # move cursor to top of screen (don't clear)
65
76
  print "\e[H"
66
77
 
67
- # header
78
+ # header (2 lines)
68
79
  draw_header
80
+ draw_base_directory_info
69
81
 
70
82
  # main content (left: directory list, right: preview)
71
83
  entries = get_display_entries
72
84
  selected_entry = entries[@keybind_handler.current_index]
73
85
 
74
- # calculate height with footer margin
75
- content_height = @screen_height - 3 # ヘッダーとフッター分を除く
86
+ # calculate height with header (2 lines) and footer margin
87
+ content_height = @screen_height - 4 # ヘッダー(2行)とフッター分を除く
76
88
  left_width = @screen_width / 2
77
89
  right_width = @screen_width - left_width
78
90
 
@@ -114,13 +126,50 @@ module Beniya
114
126
  puts "\e[7m#{header.ljust(@screen_width)}\e[0m" # reverse display
115
127
  end
116
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
+
117
166
  def draw_directory_list(entries, width, height)
118
167
  start_index = [@keybind_handler.current_index - height / 2, 0].max
119
168
  [start_index + height - 1, entries.length - 1].min
120
169
 
121
170
  (0...height).each do |i|
122
171
  entry_index = start_index + i
123
- line_num = i + 2 # skip header
172
+ line_num = i + 3 # skip header (2 lines)
124
173
 
125
174
  print "\e[#{line_num};1H" # set cursor position
126
175
 
@@ -144,16 +193,19 @@ module Beniya
144
193
  # 左ペイン専用の安全な幅を計算(右ペインにはみ出さないよう)
145
194
  safe_width = [width - 1, @screen_width / 2 - 1].min
146
195
 
196
+ # 選択マークの追加
197
+ selection_mark = @keybind_handler.is_selected?(entry[:name]) ? "✓ " : " "
198
+
147
199
  # ファイル名(必要に応じて切り詰め)
148
200
  name = entry[:name]
149
- max_name_length = safe_width - 10 # アイコンとサイズ情報分を除く
201
+ max_name_length = safe_width - 12 # アイコン、選択マーク、サイズ情報分を除く
150
202
  name = name[0...max_name_length - 3] + '...' if max_name_length > 0 && name.length > max_name_length
151
203
 
152
204
  # サイズ情報
153
205
  size_info = format_size(entry[:size])
154
206
 
155
207
  # 行の内容を構築(安全な幅内で)
156
- content_without_size = "#{icon} #{name}"
208
+ content_without_size = "#{selection_mark}#{icon} #{name}"
157
209
  available_for_content = safe_width - size_info.length
158
210
 
159
211
  line_content = if available_for_content > 0
@@ -169,7 +221,12 @@ module Beniya
169
221
  selected_color = ColorHelper.color_to_selected_ansi(ConfigLoader.colors[:selected])
170
222
  print "#{selected_color}#{line_content}#{ColorHelper.reset}"
171
223
  else
172
- 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
173
230
  end
174
231
  end
175
232
 
@@ -215,7 +272,7 @@ module Beniya
215
272
 
216
273
  def draw_file_preview(selected_entry, width, height, left_offset)
217
274
  (0...height).each do |i|
218
- line_num = i + 2
275
+ line_num = i + 3 # skip header (2 lines)
219
276
  # カーソル位置を左パネルの右端に設定
220
277
  cursor_position = left_offset + 1
221
278
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Beniya
4
- VERSION = '0.1.1'
4
+ VERSION = '0.3.0'
5
5
  end
6
-
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.1.1
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-17 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