rufio 0.81.0 → 0.82.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/CHANGELOG.md +18 -3
- data/lib/rufio/bookmark_controller.rb +439 -0
- data/lib/rufio/config_loader.rb +66 -14
- data/lib/rufio/file_operation_controller.rb +497 -0
- data/lib/rufio/keybind_handler.rb +168 -1292
- data/lib/rufio/navigation_controller.rb +266 -0
- data/lib/rufio/search_controller.rb +91 -0
- data/lib/rufio/terminal_ui.rb +66 -1103
- data/lib/rufio/ui_renderer.rb +765 -0
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +5 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12227b8ad934dc15230c756490aa8fff59aaec22a8d535a08bab0598533d52b0
|
|
4
|
+
data.tar.gz: 3fe89daae07e436eb27202a6dc3b0852daad7e45a5209a0c5178d139dfe27fc6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f614795b114ae1483947331a1f929e43cbf5b57b2e9f05923ccd0c143082836ee00740f43056279761fe36c462c5a40a9f9922de40c6ed72fc7910b332942b72
|
|
7
|
+
data.tar.gz: 85c223ae9f1a6c678e65871eafa17fe65ecdb8bf9926a09fc9fa003df6c287f78f45dee76c79fa8b14e51c96c3577969b843b9d9cb62bd5f35e55bf720ea0ef4
|
data/CHANGELOG.md
CHANGED
|
@@ -5,10 +5,12 @@ All notable changes to rufio will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.82.0] - 2026-02-28
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
-
- **Bookmark Jump Highlight**: When cycling bookmarks with `Tab`, the target bookmark in the top bar is highlighted in cyan for 500ms, then automatically fades back to gray
|
|
11
|
+
- **Bookmark Jump Highlight**: When cycling bookmarks with `Tab` or `Shift+Tab`, or jumping directly with number keys `1`–`9`, the target bookmark in the top bar is highlighted in cyan for 500ms, then automatically fades back to gray
|
|
12
|
+
- **`Shift+Tab` backward bookmark cycling**: In Files mode, `Shift+Tab` now cycles backwards through bookmarks (previously it switched tab modes)
|
|
13
|
+
- **Number key bookmark highlight**: Pressing `1`–`9` to jump to a bookmark now triggers the same 500ms cyan highlight as `Tab`; subsequent `Tab` presses continue forward from the jumped position
|
|
12
14
|
|
|
13
15
|
### Changed
|
|
14
16
|
- **UI Layout**: Merged mode tabs and title bar into a single combined bottom row
|
|
@@ -18,13 +20,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
18
20
|
- **`Tab` key**: Changed from mode cycling to **bookmark cycling** (Files mode only)
|
|
19
21
|
- In Files mode: jump to next registered bookmark in sequence (wraps to first)
|
|
20
22
|
- In other modes (Logs / Jobs / Help): no action
|
|
21
|
-
- Mode switching is done via `J` / `L` / `?` keys
|
|
23
|
+
- Mode switching is done via `J` / `L` / `?` keys
|
|
24
|
+
- **Bookmark cycling state**: `BookmarkController` now tracks `@last_bookmark_idx` to maintain cycling position independently of the current directory — navigating into a subdirectory no longer resets Tab cycling to the first bookmark
|
|
25
|
+
- **UIRenderer bookmark source**: Footer display now reads bookmarks via `@keybind_handler.bookmark_list` (same `BookmarkManager` instance as navigation) instead of creating a separate `Bookmark.new` instance pointing to the legacy `bookmarks.json`
|
|
22
26
|
|
|
23
27
|
### Fixed
|
|
28
|
+
- **Tab cycling past 3rd bookmark**: Fixed a bug where Tab always returned to the first bookmark after navigating to a subdirectory (non-bookmark directory), because `find_index` returned `nil` for non-bookmark paths
|
|
29
|
+
- **Mismatched bookmark files**: UIRenderer was reading from `~/.config/rufio/bookmarks.json` while navigation used `~/.config/rufio/bookmarks.yml`, causing the footer to display different bookmarks than Tab actually navigated to
|
|
30
|
+
- **Navigation blocked by stale bookmarks**: `goto_next_bookmark` and `goto_prev_bookmark` now skip bookmarks whose paths no longer exist (`Dir.exist?`), preventing the cycling from getting stuck on deleted directories
|
|
31
|
+
- **Test contamination of real bookmark file**: `test_bookmark_controller.rb` used `Bookmark.new` without arguments, writing test entries into the real `~/.config/rufio/bookmarks.json`; fixed to use a temp file
|
|
32
|
+
- **Number key crash**: Pressing `1`–`9` caused a `NoMethodError` crash because `goto_bookmark` was defined after `private` in `KeybindHandler`, making it inaccessible from `TerminalUI`; moved to the public section
|
|
24
33
|
- **zoxide 2-digit input**: Fixed a bug where pressing a single digit in the zoxide history dialog immediately navigated, making it impossible to enter 2-digit numbers (e.g., `12`)
|
|
25
34
|
- **Jobs mode tab highlight**: Fixed active tab not highlighting when entering Jobs mode from Logs or Help mode
|
|
26
35
|
- **Jobs mode navigation**: Fixed inability to switch away from Jobs mode using `L`, `?` keys
|
|
27
36
|
|
|
37
|
+
### Removed
|
|
38
|
+
- Deleted 3 legacy hand-written test scripts that were not part of the Minitest suite and duplicated coverage already in proper test files:
|
|
39
|
+
- `test/test_bookmark_simple.rb` — covered by `test_bookmark.rb`
|
|
40
|
+
- `test/test_filter_reset.rb` — covered by `test_filter_manager.rb`
|
|
41
|
+
- `test/test_rga_search.rb` — obsolete manual script with monkey-patching
|
|
42
|
+
|
|
28
43
|
## [0.80.0] - 2026-02-21
|
|
29
44
|
|
|
30
45
|
### Added
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# ブックマーク・zoxide・スクリプトパス専用コントローラ
|
|
5
|
+
# KeybindHandler からブックマーク系・zoxide・スクリプトパス系メソッドを分離し、単一責任原則に準拠
|
|
6
|
+
class BookmarkController
|
|
7
|
+
def initialize(directory_listing, bookmark_manager, dialog_renderer, nav_controller,
|
|
8
|
+
script_path_manager, notification_manager, zoxide_integration)
|
|
9
|
+
@directory_listing = directory_listing
|
|
10
|
+
@bookmark_manager = bookmark_manager
|
|
11
|
+
@dialog_renderer = dialog_renderer
|
|
12
|
+
@nav_controller = nav_controller
|
|
13
|
+
@script_path_manager = script_path_manager
|
|
14
|
+
@notification_manager = notification_manager
|
|
15
|
+
@zoxide_integration = zoxide_integration
|
|
16
|
+
@terminal_ui = nil
|
|
17
|
+
@last_bookmark_idx = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def set_terminal_ui(terminal_ui)
|
|
21
|
+
@terminal_ui = terminal_ui
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def set_directory_listing(directory_listing)
|
|
25
|
+
@directory_listing = directory_listing
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ============================
|
|
29
|
+
# ブックマーク操作
|
|
30
|
+
# ============================
|
|
31
|
+
|
|
32
|
+
def add_bookmark
|
|
33
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
|
34
|
+
|
|
35
|
+
bookmarks = @bookmark_manager.list
|
|
36
|
+
existing = bookmarks.find { |b| b[:path] == current_path }
|
|
37
|
+
|
|
38
|
+
if existing
|
|
39
|
+
content_lines = [
|
|
40
|
+
'',
|
|
41
|
+
'This directory is already bookmarked',
|
|
42
|
+
"Name: #{existing[:name]}",
|
|
43
|
+
'',
|
|
44
|
+
'Press any key to continue...'
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
title = 'Bookmark Exists'
|
|
48
|
+
width = 50
|
|
49
|
+
height = content_lines.length + 4
|
|
50
|
+
|
|
51
|
+
show_overlay_dialog(title, content_lines, {
|
|
52
|
+
width: width,
|
|
53
|
+
height: height,
|
|
54
|
+
border_color: "\e[33m",
|
|
55
|
+
title_color: "\e[1;33m",
|
|
56
|
+
content_color: "\e[37m"
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
dir_name = File.basename(current_path)
|
|
63
|
+
|
|
64
|
+
title = "Add Bookmark: #{dir_name}"
|
|
65
|
+
prompt = "Enter bookmark name:"
|
|
66
|
+
bookmark_name = @dialog_renderer.show_input_dialog(title, prompt, {
|
|
67
|
+
border_color: "\e[32m",
|
|
68
|
+
title_color: "\e[1;32m",
|
|
69
|
+
content_color: "\e[37m"
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return false if bookmark_name.nil? || bookmark_name.empty?
|
|
73
|
+
|
|
74
|
+
result = @bookmark_manager.add(current_path, bookmark_name)
|
|
75
|
+
|
|
76
|
+
@terminal_ui&.refresh_display
|
|
77
|
+
result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def show_bookmark_menu
|
|
81
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
|
82
|
+
|
|
83
|
+
menu_items = [
|
|
84
|
+
'1. Add current dir to bookmarks',
|
|
85
|
+
'2. Add to script paths',
|
|
86
|
+
'3. Manage script paths',
|
|
87
|
+
'4. View bookmarks'
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
content_lines = [''] + menu_items + ['', '[1-4] Select | [Esc] Cancel']
|
|
91
|
+
|
|
92
|
+
title = 'Bookmark Menu'
|
|
93
|
+
width = 45
|
|
94
|
+
height = content_lines.length + 4
|
|
95
|
+
|
|
96
|
+
key = show_overlay_dialog(title, content_lines, {
|
|
97
|
+
width: width,
|
|
98
|
+
height: height,
|
|
99
|
+
border_color: "\e[36m",
|
|
100
|
+
title_color: "\e[1;36m",
|
|
101
|
+
content_color: "\e[37m"
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
case key
|
|
105
|
+
when '1'
|
|
106
|
+
add_bookmark
|
|
107
|
+
when '2'
|
|
108
|
+
add_to_script_paths
|
|
109
|
+
when '3'
|
|
110
|
+
show_script_paths_manager
|
|
111
|
+
when '4'
|
|
112
|
+
selected_bookmark = @bookmark_manager.list_interactive
|
|
113
|
+
@terminal_ui&.refresh_display
|
|
114
|
+
if selected_bookmark
|
|
115
|
+
if @bookmark_manager.path_exists?(selected_bookmark)
|
|
116
|
+
navigate_to_directory(selected_bookmark[:path])
|
|
117
|
+
else
|
|
118
|
+
show_error_and_wait('bookmark.path_not_exist', selected_bookmark[:path])
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
@terminal_ui&.refresh_display
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def goto_bookmark(number)
|
|
129
|
+
bookmark = @bookmark_manager.find_by_number(number)
|
|
130
|
+
|
|
131
|
+
return false unless bookmark
|
|
132
|
+
return false unless @bookmark_manager.path_exists?(bookmark)
|
|
133
|
+
|
|
134
|
+
# Tab循環位置を更新(数字キー後の Tab が次の番号から続くように)
|
|
135
|
+
@last_bookmark_idx = number - 1
|
|
136
|
+
navigate_to_directory(bookmark[:path])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# パスを指定してブックマーク移動(テスト用)
|
|
140
|
+
def goto_bookmark_by_path(path)
|
|
141
|
+
navigate_to_directory(path)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def goto_start_directory
|
|
145
|
+
start_dir = @directory_listing&.start_directory
|
|
146
|
+
return false unless start_dir
|
|
147
|
+
|
|
148
|
+
navigate_to_directory(start_dir)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# 次のブックマークに移動(Tabキー)
|
|
152
|
+
# 現在のディレクトリがブックマーク内なら current_path を基準に次へ、
|
|
153
|
+
# そうでない場合は @last_bookmark_idx から継続(ブックマーク外に出ても循環が止まらない)
|
|
154
|
+
# 存在しないパスのブックマークはスキップする
|
|
155
|
+
# @return [Integer, nil] 新しいブックマークインデックス、またはnil
|
|
156
|
+
def goto_next_bookmark
|
|
157
|
+
bookmarks = @bookmark_manager.list
|
|
158
|
+
return nil unless bookmarks&.any?
|
|
159
|
+
|
|
160
|
+
current_path = @directory_listing.current_path
|
|
161
|
+
current_position = bookmarks.find_index { |bm| bm[:path] == current_path }
|
|
162
|
+
start_idx = if current_position
|
|
163
|
+
(current_position + 1) % bookmarks.length
|
|
164
|
+
elsif @last_bookmark_idx
|
|
165
|
+
(@last_bookmark_idx + 1) % bookmarks.length
|
|
166
|
+
else
|
|
167
|
+
0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
bookmarks.length.times do |i|
|
|
171
|
+
idx = (start_idx + i) % bookmarks.length
|
|
172
|
+
next unless Dir.exist?(bookmarks[idx][:path])
|
|
173
|
+
|
|
174
|
+
@last_bookmark_idx = idx
|
|
175
|
+
navigate_to_directory(bookmarks[idx][:path])
|
|
176
|
+
return @last_bookmark_idx
|
|
177
|
+
end
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# 前のブックマークに移動(Shift+Tabキー)
|
|
182
|
+
# 存在しないパスのブックマークはスキップする
|
|
183
|
+
# @return [Integer, nil] 新しいブックマークインデックス、またはnil
|
|
184
|
+
def goto_prev_bookmark
|
|
185
|
+
bookmarks = @bookmark_manager.list
|
|
186
|
+
return nil unless bookmarks&.any?
|
|
187
|
+
|
|
188
|
+
current_path = @directory_listing.current_path
|
|
189
|
+
current_position = bookmarks.find_index { |bm| bm[:path] == current_path }
|
|
190
|
+
start_idx = if current_position
|
|
191
|
+
(current_position - 1 + bookmarks.length) % bookmarks.length
|
|
192
|
+
elsif @last_bookmark_idx
|
|
193
|
+
(@last_bookmark_idx - 1 + bookmarks.length) % bookmarks.length
|
|
194
|
+
else
|
|
195
|
+
bookmarks.length - 1
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
bookmarks.length.times do |i|
|
|
199
|
+
idx = (start_idx - i + bookmarks.length) % bookmarks.length
|
|
200
|
+
next unless Dir.exist?(bookmarks[idx][:path])
|
|
201
|
+
|
|
202
|
+
@last_bookmark_idx = idx
|
|
203
|
+
navigate_to_directory(bookmarks[idx][:path])
|
|
204
|
+
return @last_bookmark_idx
|
|
205
|
+
end
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# ============================
|
|
210
|
+
# zoxide 連携
|
|
211
|
+
# ============================
|
|
212
|
+
|
|
213
|
+
def show_zoxide_menu
|
|
214
|
+
selected_path = @zoxide_integration.show_menu
|
|
215
|
+
|
|
216
|
+
if selected_path && Dir.exist?(selected_path)
|
|
217
|
+
if navigate_to_directory(selected_path)
|
|
218
|
+
@zoxide_integration.add_to_history(selected_path)
|
|
219
|
+
true
|
|
220
|
+
else
|
|
221
|
+
false
|
|
222
|
+
end
|
|
223
|
+
else
|
|
224
|
+
@terminal_ui&.refresh_display
|
|
225
|
+
false
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# ============================
|
|
230
|
+
# スクリプトパス管理
|
|
231
|
+
# ============================
|
|
232
|
+
|
|
233
|
+
def add_to_script_paths
|
|
234
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
|
235
|
+
|
|
236
|
+
@script_path_manager ||= ScriptPathManager.new(Config::SCRIPT_PATHS_YML)
|
|
237
|
+
|
|
238
|
+
if @script_path_manager.paths.include?(current_path)
|
|
239
|
+
show_notification('Already in paths', current_path, :info)
|
|
240
|
+
elsif @script_path_manager.add_path(current_path)
|
|
241
|
+
show_notification('Added to scripts', current_path, :success)
|
|
242
|
+
else
|
|
243
|
+
show_notification('Failed to add', current_path, :error)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
@terminal_ui&.refresh_display
|
|
247
|
+
true
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def show_script_paths_manager
|
|
251
|
+
unless @script_path_manager
|
|
252
|
+
show_notification('No script paths configured', '', :info)
|
|
253
|
+
@terminal_ui&.refresh_display
|
|
254
|
+
return false
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
paths = @script_path_manager.paths
|
|
258
|
+
if paths.empty?
|
|
259
|
+
show_notification('No script paths', 'Press B > 2 to add', :info)
|
|
260
|
+
@terminal_ui&.refresh_display
|
|
261
|
+
return false
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
selected_index = 0
|
|
265
|
+
screen = @terminal_ui&.screen
|
|
266
|
+
renderer = @terminal_ui&.renderer
|
|
267
|
+
|
|
268
|
+
loop do
|
|
269
|
+
menu_items = paths.each_with_index.map do |path, i|
|
|
270
|
+
prefix = i == selected_index ? '> ' : ' '
|
|
271
|
+
"#{prefix}#{i + 1}. #{truncate_path(path, 35)}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
content_lines = [''] + menu_items + ['', '[j/k] Move | [d] Delete | [Enter] Jump | [Esc] Back']
|
|
275
|
+
|
|
276
|
+
title = 'Script Paths'
|
|
277
|
+
width = 50
|
|
278
|
+
height = content_lines.length + 4
|
|
279
|
+
|
|
280
|
+
if screen && renderer
|
|
281
|
+
screen.enable_overlay
|
|
282
|
+
x, y = @dialog_renderer.calculate_center(width, height)
|
|
283
|
+
@dialog_renderer.draw_floating_window_to_overlay(screen, x, y, width, height, title, content_lines, {
|
|
284
|
+
border_color: "\e[35m",
|
|
285
|
+
title_color: "\e[1;35m",
|
|
286
|
+
content_color: "\e[37m"
|
|
287
|
+
})
|
|
288
|
+
renderer.render(screen)
|
|
289
|
+
|
|
290
|
+
key = STDIN.getch
|
|
291
|
+
screen.disable_overlay
|
|
292
|
+
renderer.render(screen)
|
|
293
|
+
else
|
|
294
|
+
x, y = @dialog_renderer.calculate_center(width, height)
|
|
295
|
+
@dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
|
|
296
|
+
border_color: "\e[35m",
|
|
297
|
+
title_color: "\e[1;35m",
|
|
298
|
+
content_color: "\e[37m"
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
key = STDIN.getch
|
|
302
|
+
@dialog_renderer.clear_area(x, y, width, height)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
case key
|
|
306
|
+
when 'j'
|
|
307
|
+
selected_index = [selected_index + 1, paths.length - 1].min
|
|
308
|
+
when 'k'
|
|
309
|
+
selected_index = [selected_index - 1, 0].max
|
|
310
|
+
when 'd'
|
|
311
|
+
path_to_delete = paths[selected_index]
|
|
312
|
+
if confirm_delete_script_path(path_to_delete)
|
|
313
|
+
@script_path_manager.remove_path(path_to_delete)
|
|
314
|
+
paths = @script_path_manager.paths
|
|
315
|
+
selected_index = [selected_index, paths.length - 1].min
|
|
316
|
+
break if paths.empty?
|
|
317
|
+
end
|
|
318
|
+
when "\r", "\n"
|
|
319
|
+
path = paths[selected_index]
|
|
320
|
+
navigate_to_directory(path) if Dir.exist?(path)
|
|
321
|
+
break
|
|
322
|
+
when "\e"
|
|
323
|
+
break
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
@terminal_ui&.refresh_display
|
|
328
|
+
true
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def confirm_delete_script_path(path)
|
|
332
|
+
content_lines = [
|
|
333
|
+
'',
|
|
334
|
+
'Delete this script path?',
|
|
335
|
+
'',
|
|
336
|
+
truncate_path(path, 40),
|
|
337
|
+
'',
|
|
338
|
+
'[y] Yes | [n] No'
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
title = 'Confirm Delete'
|
|
342
|
+
width = 50
|
|
343
|
+
height = content_lines.length + 4
|
|
344
|
+
|
|
345
|
+
key = show_overlay_dialog(title, content_lines, {
|
|
346
|
+
width: width,
|
|
347
|
+
height: height,
|
|
348
|
+
border_color: "\e[31m",
|
|
349
|
+
title_color: "\e[1;31m",
|
|
350
|
+
content_color: "\e[37m"
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
key.downcase == 'y'
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# ============================
|
|
357
|
+
# ナビゲーション(委譲)
|
|
358
|
+
# ============================
|
|
359
|
+
|
|
360
|
+
def navigate_to_directory(path)
|
|
361
|
+
return false unless @directory_listing
|
|
362
|
+
|
|
363
|
+
result = @directory_listing.navigate_to_path(path)
|
|
364
|
+
if result
|
|
365
|
+
@nav_controller.select_index(0)
|
|
366
|
+
@nav_controller.clear_filter_mode
|
|
367
|
+
true
|
|
368
|
+
else
|
|
369
|
+
false
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
private
|
|
374
|
+
|
|
375
|
+
def show_overlay_dialog(title, content_lines, options = {}, &block)
|
|
376
|
+
use_overlay = @terminal_ui &&
|
|
377
|
+
@terminal_ui.respond_to?(:screen) &&
|
|
378
|
+
@terminal_ui.respond_to?(:renderer) &&
|
|
379
|
+
@terminal_ui.screen &&
|
|
380
|
+
@terminal_ui.renderer
|
|
381
|
+
|
|
382
|
+
if use_overlay
|
|
383
|
+
@terminal_ui.show_overlay_dialog(title, content_lines, options, &block)
|
|
384
|
+
else
|
|
385
|
+
width = options[:width]
|
|
386
|
+
height = options[:height]
|
|
387
|
+
|
|
388
|
+
unless width && height
|
|
389
|
+
width, height = @dialog_renderer.calculate_dimensions(content_lines, {
|
|
390
|
+
title: title,
|
|
391
|
+
min_width: options[:min_width] || 40,
|
|
392
|
+
max_width: options[:max_width] || 80
|
|
393
|
+
})
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
x, y = @dialog_renderer.calculate_center(width, height)
|
|
397
|
+
|
|
398
|
+
@dialog_renderer.draw_floating_window(x, y, width, height, title, content_lines, {
|
|
399
|
+
border_color: options[:border_color] || "\e[37m",
|
|
400
|
+
title_color: options[:title_color] || "\e[1;33m",
|
|
401
|
+
content_color: options[:content_color] || "\e[37m"
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
key = block_given? ? yield : STDIN.getch
|
|
405
|
+
|
|
406
|
+
@dialog_renderer.clear_area(x, y, width, height)
|
|
407
|
+
@terminal_ui&.refresh_display
|
|
408
|
+
|
|
409
|
+
key
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def show_notification(title, message, type)
|
|
414
|
+
return unless @notification_manager
|
|
415
|
+
|
|
416
|
+
@notification_manager.add(title, type, duration: 3, exit_code: nil)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def show_error_and_wait(message_key, value)
|
|
420
|
+
puts "\n#{ConfigLoader.message(message_key) || message_key}: #{value}"
|
|
421
|
+
wait_for_keypress
|
|
422
|
+
false
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def wait_for_keypress
|
|
426
|
+
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
|
427
|
+
STDIN.getch
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def truncate_path(path, max_length)
|
|
431
|
+
return path if path.length <= max_length
|
|
432
|
+
|
|
433
|
+
display_path = path.sub(Dir.home, '~')
|
|
434
|
+
return display_path if display_path.length <= max_length
|
|
435
|
+
|
|
436
|
+
"...#{display_path[-(max_length - 3)..]}"
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
data/lib/rufio/config_loader.rb
CHANGED
|
@@ -38,8 +38,66 @@ module Rufio
|
|
|
38
38
|
load_config[:colors]
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# デフォルトキーバインド(全アクションの単一キー定義)
|
|
42
|
+
def default_keybinds
|
|
43
|
+
{
|
|
44
|
+
# ナビゲーション
|
|
45
|
+
move_up: 'k',
|
|
46
|
+
move_down: 'j',
|
|
47
|
+
navigate_parent: 'h',
|
|
48
|
+
navigate_enter: 'l',
|
|
49
|
+
top: 'g',
|
|
50
|
+
bottom: 'G',
|
|
51
|
+
refresh: 'R',
|
|
52
|
+
# ファイル操作
|
|
53
|
+
open_file: 'o',
|
|
54
|
+
rename: 'r',
|
|
55
|
+
delete: 'd',
|
|
56
|
+
create_file: 'a',
|
|
57
|
+
create_dir: 'A',
|
|
58
|
+
move_selected: 'm',
|
|
59
|
+
copy_selected: 'c',
|
|
60
|
+
delete_selected: 'x',
|
|
61
|
+
open_explorer: 'e',
|
|
62
|
+
# 選択・検索
|
|
63
|
+
select: ' ',
|
|
64
|
+
filter: 'f',
|
|
65
|
+
fzf_search: 's',
|
|
66
|
+
fzf_search_alt: '/',
|
|
67
|
+
rga_search: 'F',
|
|
68
|
+
# ブックマーク
|
|
69
|
+
add_bookmark: 'b',
|
|
70
|
+
bookmark_menu: 'B',
|
|
71
|
+
zoxide: 'z',
|
|
72
|
+
start_dir: '0',
|
|
73
|
+
# モード・ツール
|
|
74
|
+
job_mode: 'J',
|
|
75
|
+
help: '?',
|
|
76
|
+
log_viewer: 'L',
|
|
77
|
+
command_mode: ':',
|
|
78
|
+
quit: 'q'
|
|
79
|
+
}.freeze
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# デフォルトUI設定
|
|
83
|
+
def default_ui_options
|
|
84
|
+
{
|
|
85
|
+
panel_ratio: 0.5, # 左パネル幅比率(0.3〜0.7)
|
|
86
|
+
preview_enabled: true # ファイルプレビューのON/OFF
|
|
87
|
+
}.freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# UI設定(デフォルト+ユーザー設定のマージ)
|
|
91
|
+
def ui_options
|
|
92
|
+
user_opts = load_config[:ui_options] || {}
|
|
93
|
+
default_ui_options.merge(user_opts)
|
|
94
|
+
end
|
|
95
|
+
|
|
41
96
|
def keybinds
|
|
42
|
-
load_config[:keybinds]
|
|
97
|
+
raw_user_keybinds = load_config[:keybinds] || {}
|
|
98
|
+
# 新フォーマット(単一文字列値)のみ受け入れ、古いフォーマット(配列値)は無視
|
|
99
|
+
user_keybinds = raw_user_keybinds.select { |_, v| v.is_a?(String) }
|
|
100
|
+
default_keybinds.merge(user_keybinds)
|
|
43
101
|
end
|
|
44
102
|
|
|
45
103
|
def language
|
|
@@ -228,7 +286,7 @@ module Rufio
|
|
|
228
286
|
config = {
|
|
229
287
|
applications: safe_const_get(:APPLICATIONS, default_config[:applications]),
|
|
230
288
|
colors: safe_const_get(:COLORS, default_config[:colors]),
|
|
231
|
-
keybinds: safe_const_get(:KEYBINDS,
|
|
289
|
+
keybinds: safe_const_get(:KEYBINDS, {})
|
|
232
290
|
}
|
|
233
291
|
|
|
234
292
|
if Object.const_defined?(:LANGUAGE)
|
|
@@ -245,6 +303,10 @@ module Rufio
|
|
|
245
303
|
config[:command_history_size] = Object.const_get(:COMMAND_HISTORY_SIZE)
|
|
246
304
|
end
|
|
247
305
|
|
|
306
|
+
if Object.const_defined?(:UI_OPTIONS)
|
|
307
|
+
config[:ui_options] = safe_const_get(:UI_OPTIONS, {})
|
|
308
|
+
end
|
|
309
|
+
|
|
248
310
|
config
|
|
249
311
|
rescue StandardError => e
|
|
250
312
|
warn "Failed to load config file: #{e.message}"
|
|
@@ -273,18 +335,8 @@ module Rufio
|
|
|
273
335
|
selected: { hsl: [50, 90, 70] },
|
|
274
336
|
preview: { hsl: [180, 60, 65] }
|
|
275
337
|
},
|
|
276
|
-
keybinds: {
|
|
277
|
-
|
|
278
|
-
up: %w[k UP],
|
|
279
|
-
down: %w[j DOWN],
|
|
280
|
-
left: %w[h LEFT],
|
|
281
|
-
right: %w[l RIGHT ENTER],
|
|
282
|
-
top: %w[g],
|
|
283
|
-
bottom: %w[G],
|
|
284
|
-
refresh: %w[r],
|
|
285
|
-
search: %w[/],
|
|
286
|
-
open_file: %w[o SPACE]
|
|
287
|
-
}
|
|
338
|
+
keybinds: {}, # ユーザーの上書きのみ格納(デフォルトはdefault_keybindsを参照)
|
|
339
|
+
ui_options: {} # ユーザーの上書きのみ格納(デフォルトはdefault_ui_optionsを参照)
|
|
288
340
|
}
|
|
289
341
|
end
|
|
290
342
|
end
|