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 +4 -4
- data/README.md +62 -11
- data/README_EN.md +62 -11
- data/lib/beniya/application.rb +1 -0
- data/lib/beniya/config.rb +18 -0
- data/lib/beniya/file_opener.rb +26 -0
- data/lib/beniya/file_preview.rb +23 -3
- data/lib/beniya/keybind_handler.rb +274 -20
- data/lib/beniya/terminal_ui.rb +65 -8
- data/lib/beniya/version.rb +1 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e937b12eb5f1aa3425c8dd832cb7bd11396dfd86b6e232de58fe771c3da47a2
|
4
|
+
data.tar.gz: 1c41f33e6419d9458c778e8d7937472ad29e4f6d49ffcca12f1e78fac07f16ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
-
|
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
|
-
| `
|
95
|
+
| `s` | フィルターモード開始・再編集 |
|
83
96
|
| 文字入力 | ファイル名で絞り込み(フィルターモード中) |
|
84
97
|
| `Enter` | フィルター維持で通常モードに戻る |
|
85
98
|
| `ESC` | フィルターをクリアして通常モードに戻る |
|
@@ -89,8 +102,8 @@ beniya --help # ヘルプメッセージを表示
|
|
89
102
|
|
90
103
|
| キー | 機能 |
|
91
104
|
| ---- | ----------------------------------------- |
|
92
|
-
|
|
93
|
-
| `
|
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
|
-
#### リアルタイムフィルター (`
|
154
|
+
#### リアルタイムフィルター (`s`)
|
104
155
|
|
105
|
-
- **フィルター開始**: `
|
156
|
+
- **フィルター開始**: `s`キーを押してフィルターモードに入る
|
106
157
|
- **文字入力による絞り込み**: 日本語・英数字・記号に対応
|
107
158
|
- **リアルタイム更新**: 文字を入力するたびに表示が更新される
|
108
159
|
- **フィルター維持**: `Enter`キーでフィルターを保持したまま通常操作に戻る
|
109
160
|
- **フィルタークリア**: `ESC`キーでフィルターをクリアして通常表示に戻る
|
110
|
-
- **再編集**: フィルター適用中に`
|
161
|
+
- **再編集**: フィルター適用中に`s`キーで再編集モード
|
111
162
|
- **文字削除**: `Backspace`で文字削除、全削除時は自動でフィルタークリア
|
112
163
|
|
113
164
|
#### 使用例
|
114
165
|
|
115
166
|
```
|
116
|
-
1.
|
167
|
+
1. s → フィルターモード開始
|
117
168
|
2. ".rb" → Rubyファイルのみ表示
|
118
169
|
3. Enter → フィルター維持で通常操作
|
119
170
|
4. j/k → フィルター結果内をナビゲーション
|
120
|
-
5.
|
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
|
-
#### ファイル内容検索 (`
|
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
|
-
- **
|
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
|
-
| `
|
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
|
-
|
|
93
|
-
| `
|
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 (`
|
154
|
+
#### Real-time Filter (`s`)
|
104
155
|
|
105
|
-
- **Start Filter**: Press `
|
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 `
|
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.
|
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.
|
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 (`
|
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
|
data/lib/beniya/application.rb
CHANGED
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' => '操作: ',
|
data/lib/beniya/file_opener.rb
CHANGED
@@ -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
|
|
data/lib/beniya/file_preview.rb
CHANGED
@@ -19,8 +19,8 @@ module Beniya
|
|
19
19
|
|
20
20
|
begin
|
21
21
|
# binary file detection
|
22
|
-
sample = File.
|
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 '
|
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
|
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
|
data/lib/beniya/terminal_ui.rb
CHANGED
@@ -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 -
|
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 +
|
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 -
|
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
|
-
|
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
|
|
data/lib/beniya/version.rb
CHANGED
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.
|
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-
|
10
|
+
date: 2025-09-06 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: io-console
|