rufio 1.1.0 → 1.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/CHANGELOG.md +34 -0
- data/README.md +74 -3
- data/README_ja.md +71 -2
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/info/help.md +2 -0
- data/info/keybindings.md +3 -1
- data/info/welcome.md +1 -1
- data/lib/rufio/application.rb +12 -1
- data/lib/rufio/archive_controller.rb +211 -0
- data/lib/rufio/background_command_executor.rb +9 -0
- data/lib/rufio/background_status.rb +49 -0
- data/lib/rufio/bookmark_controller.rb +38 -121
- data/lib/rufio/bookmark_manager.rb +77 -331
- data/lib/rufio/command_completion.rb +6 -1
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +40 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +3 -11
- data/lib/rufio/dialog_renderer.rb +64 -152
- data/lib/rufio/dialog_support.rb +69 -0
- data/lib/rufio/document_text_extractor.rb +167 -0
- data/lib/rufio/file_operation_controller.rb +49 -222
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +121 -9
- data/lib/rufio/help_menu.rb +8 -2
- data/lib/rufio/interpreter_resolver.rb +6 -11
- data/lib/rufio/job_manager.rb +2 -5
- data/lib/rufio/key_input.rb +77 -0
- data/lib/rufio/keybind_handler.rb +81 -115
- data/lib/rufio/local_script_scanner.rb +6 -18
- data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native_content_search.rb +153 -0
- data/lib/rufio/native_scanner.rb +13 -68
- data/lib/rufio/native_scanner_zig.rb +106 -56
- data/lib/rufio/native_zip.rb +121 -0
- data/lib/rufio/navigation_controller.rb +18 -20
- data/lib/rufio/screen.rb +2 -12
- data/lib/rufio/script_executor.rb +6 -73
- data/lib/rufio/script_file.rb +44 -0
- data/lib/rufio/script_path_manager.rb +9 -43
- data/lib/rufio/script_runner.rb +13 -50
- data/lib/rufio/search_controller.rb +1 -1
- data/lib/rufio/shell_command_completion.rb +1 -5
- data/lib/rufio/shell_init.rb +90 -0
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +55 -294
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +63 -40
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio/zip_archive.rb +314 -0
- data/lib/rufio/zip_extractor.rb +346 -0
- data/lib/rufio/zip_writer.rb +268 -0
- data/lib/rufio/zoxide_integration.rb +5 -43
- data/lib/rufio.rb +5 -0
- data/lib_zig/rufio_native/Makefile +54 -17
- data/lib_zig/rufio_native/bench.sh +36 -0
- data/lib_zig/rufio_native/build.zig +32 -35
- data/lib_zig/rufio_native/src/cli.zig +99 -0
- data/lib_zig/rufio_native/src/main.zig +187 -83
- data/lib_zig/rufio_native/src/os.zig +889 -0
- data/lib_zig/rufio_native/src/search.zig +919 -0
- data/lib_zig/rufio_native/src/search_test.zig +88 -0
- data/lib_zig/rufio_native/src/zip.zig +482 -0
- metadata +125 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
data/lib/rufio/application.rb
CHANGED
|
@@ -7,8 +7,10 @@ module Rufio
|
|
|
7
7
|
# Error display constants
|
|
8
8
|
BACKTRACE_LINES = 5 # Number of backtrace lines to show
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# choosedir: T キーで終了した時に最後のディレクトリを書き出すファイル(rufio --shell-init の関数が使う)
|
|
11
|
+
def initialize(start_directory = Dir.pwd, choosedir: nil)
|
|
11
12
|
@start_directory = File.expand_path(start_directory)
|
|
13
|
+
@choosedir = choosedir
|
|
12
14
|
# Load configuration including language settings
|
|
13
15
|
ConfigLoader.load_config
|
|
14
16
|
end
|
|
@@ -28,12 +30,21 @@ module Rufio
|
|
|
28
30
|
|
|
29
31
|
# アプリケーション開始
|
|
30
32
|
terminal_ui.start(directory_listing, keybind_handler, file_preview, background_executor)
|
|
33
|
+
write_choosedir(terminal_ui.exit_directory)
|
|
31
34
|
rescue Interrupt
|
|
32
35
|
puts "\n\n#{ConfigLoader.message('app.interrupted')}"
|
|
33
36
|
rescue StandardError => e
|
|
34
37
|
puts "\n#{ConfigLoader.message('app.error_occurred')}: #{e.message}"
|
|
35
38
|
puts e.backtrace.first(BACKTRACE_LINES).join("\n")
|
|
36
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def write_choosedir(directory)
|
|
44
|
+
return unless @choosedir && directory
|
|
45
|
+
|
|
46
|
+
File.write(@choosedir, directory)
|
|
47
|
+
end
|
|
37
48
|
end
|
|
38
49
|
end
|
|
39
50
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'dialog_support'
|
|
4
|
+
require_relative 'zip_extractor'
|
|
5
|
+
require_relative 'zip_writer'
|
|
6
|
+
|
|
7
|
+
module Rufio
|
|
8
|
+
# 選択中のアイテムを z キーで圧縮・展開するコントローラ
|
|
9
|
+
#
|
|
10
|
+
# - 選択がすべて .zip ファイルなら、それぞれを現在のディレクトリの <名前>/ に展開する
|
|
11
|
+
# - それ以外は選択アイテムを 1 つの ZIP にまとめて現在のディレクトリに作成する
|
|
12
|
+
# 処理は JobManager のジョブとしてバックグラウンドで実行し、Jobs モードからキャンセルできる。
|
|
13
|
+
class ArchiveController
|
|
14
|
+
include DialogSupport
|
|
15
|
+
|
|
16
|
+
DEFAULT_ARCHIVE_NAME = 'Archive.zip'
|
|
17
|
+
DIALOG_WIDTH = 50
|
|
18
|
+
DIALOG_BORDER_HEIGHT = 4
|
|
19
|
+
PROGRESS_STEP = 5 # 進捗表示を更新する間隔(%)
|
|
20
|
+
|
|
21
|
+
Plan = Struct.new(:action, :source_dir, :dest_dir, :items, :archives, :default_name, keyword_init: true)
|
|
22
|
+
|
|
23
|
+
def initialize(directory_listing, dialog_renderer, selection_manager, job_manager, notification_manager)
|
|
24
|
+
@directory_listing = directory_listing
|
|
25
|
+
@dialog_renderer = dialog_renderer
|
|
26
|
+
@selection_manager = selection_manager
|
|
27
|
+
@job_manager = job_manager
|
|
28
|
+
@notification_manager = notification_manager
|
|
29
|
+
@terminal_ui = nil
|
|
30
|
+
@mutex = Mutex.new
|
|
31
|
+
@finished_dirs = []
|
|
32
|
+
@progress_changed = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set_terminal_ui(terminal_ui)
|
|
36
|
+
@terminal_ui = terminal_ui
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_directory_listing(directory_listing)
|
|
40
|
+
@directory_listing = directory_listing
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 選択アイテムから実行内容を決める(UI に依存しない)
|
|
44
|
+
def self.plan(source_dir, items, dest_dir)
|
|
45
|
+
items = items.reject { |item| item == '..' }
|
|
46
|
+
return nil if items.empty?
|
|
47
|
+
|
|
48
|
+
paths = items.map { |item| File.join(source_dir, item) }
|
|
49
|
+
if paths.all? { |path| File.file?(path) && File.extname(path).casecmp?('.zip') }
|
|
50
|
+
archives = paths.map { |path| [path, unique_path(dest_dir, File.basename(path, '.*'), '')] }
|
|
51
|
+
Plan.new(action: :extract, source_dir: source_dir, dest_dir: dest_dir, items: items, archives: archives)
|
|
52
|
+
else
|
|
53
|
+
default_name = items.length == 1 ? "#{File.basename(items.first)}.zip" : DEFAULT_ARCHIVE_NAME
|
|
54
|
+
Plan.new(action: :compress, source_dir: source_dir, dest_dir: dest_dir, items: items, default_name: default_name)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# dir/base<ext> が既にあれば "base 2<ext>", "base 3<ext>" ... を返す
|
|
59
|
+
def self.unique_path(dir, base, ext)
|
|
60
|
+
candidate = File.join(dir, "#{base}#{ext}")
|
|
61
|
+
number = 2
|
|
62
|
+
while File.exist?(candidate) || File.symlink?(candidate)
|
|
63
|
+
candidate = File.join(dir, "#{base} #{number}#{ext}")
|
|
64
|
+
number += 1
|
|
65
|
+
end
|
|
66
|
+
candidate
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 入力されたアーカイブ名を検証して .zip を補う(不正なら nil)
|
|
70
|
+
def self.archive_name(input, default_name)
|
|
71
|
+
name = input.to_s.strip
|
|
72
|
+
name = default_name if name.empty?
|
|
73
|
+
return nil if name.match?(%r{[/\\\0]}) || ['.', '..'].include?(name)
|
|
74
|
+
|
|
75
|
+
File.extname(name).casecmp?('.zip') ? name : "#{name}.zip"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# z キー(選択あり)の入口
|
|
79
|
+
def archive_selected
|
|
80
|
+
return false if @selection_manager.empty?
|
|
81
|
+
|
|
82
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
|
83
|
+
source_dir = @selection_manager.source_directory || current_path
|
|
84
|
+
plan = self.class.plan(source_dir, @selection_manager.selected_items, current_path)
|
|
85
|
+
return false unless plan
|
|
86
|
+
|
|
87
|
+
started = plan.action == :extract ? confirm_and_extract(plan) : prompt_and_compress(plan)
|
|
88
|
+
@selection_manager.clear if started
|
|
89
|
+
@terminal_ui&.refresh_display
|
|
90
|
+
started
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def start_extract(zip_path, dest_dir)
|
|
94
|
+
run_job("Extract #{File.basename(zip_path)}", [File.dirname(dest_dir), dest_dir]) do |job, progress|
|
|
95
|
+
created = !File.exist?(dest_dir)
|
|
96
|
+
begin
|
|
97
|
+
result = ZipExtractor.new(zip_path, dest_dir, progress: progress, cancelled: -> { job.cancelled? }).extract
|
|
98
|
+
rescue StandardError
|
|
99
|
+
# 新しく作った展開先だけを片付ける(既存ディレクトリには触れない)
|
|
100
|
+
FileUtils.rm_rf(dest_dir) if created
|
|
101
|
+
raise
|
|
102
|
+
end
|
|
103
|
+
job.append_log("Extracted #{result.extracted.length} item(s) to #{dest_dir}")
|
|
104
|
+
result.skipped.each { |skip| job.append_log("Skipped #{skip[:name]} (#{skip[:reason]})") }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def start_compress(zip_path, source_dir, items)
|
|
109
|
+
run_job("Compress #{File.basename(zip_path)}", [File.dirname(zip_path)]) do |job, progress|
|
|
110
|
+
result = ZipWriter.new(zip_path, source_dir, items, progress: progress, cancelled: -> { job.cancelled? }).write
|
|
111
|
+
job.append_log("Created #{zip_path} with #{result.added.length} item(s)")
|
|
112
|
+
result.skipped.each { |skip| job.append_log("Skipped #{skip[:name]} (#{skip[:reason]})") }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# メインループから呼ぶ。表示を更新すべき変化があれば true
|
|
117
|
+
def poll
|
|
118
|
+
finished, changed = @mutex.synchronize do
|
|
119
|
+
dirs = @finished_dirs
|
|
120
|
+
@finished_dirs = []
|
|
121
|
+
progress = @progress_changed
|
|
122
|
+
@progress_changed = false
|
|
123
|
+
[dirs, progress || !dirs.empty?]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
current = @directory_listing&.current_path
|
|
127
|
+
@directory_listing.refresh if current && finished.any? { |dir| File.expand_path(dir) == File.expand_path(current) }
|
|
128
|
+
changed
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def confirm_and_extract(plan)
|
|
134
|
+
lines = ['', "Extract #{plan.archives.length} archive(s)?", '']
|
|
135
|
+
plan.archives.first(5).each do |zip, dest|
|
|
136
|
+
lines << " #{TextUtils.shorten_path_from_end(File.basename(zip), 20)} -> #{TextUtils.shorten_path_from_end(File.basename(dest), 20)}/"
|
|
137
|
+
end
|
|
138
|
+
lines << " ... #{plan.archives.length - 5} more" if plan.archives.length > 5
|
|
139
|
+
lines += ['', "To: #{TextUtils.shorten_path_from_end(plan.dest_dir, 40)}", '', ' [Y]es - Extract', ' [N]o - Cancel', '']
|
|
140
|
+
|
|
141
|
+
return false unless confirm_dialog('Extract Archives', lines, dialog_options(lines))
|
|
142
|
+
|
|
143
|
+
plan.archives.each { |zip, dest| start_extract(zip, dest) }
|
|
144
|
+
true
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def prompt_and_compress(plan)
|
|
148
|
+
input = @dialog_renderer.show_input_dialog(
|
|
149
|
+
"Compress #{plan.items.length} item(s)",
|
|
150
|
+
"Archive name (Enter: #{plan.default_name}):",
|
|
151
|
+
{ width: DIALOG_WIDTH, border_color: "\e[36m", title_color: "\e[1;36m", content_color: "\e[37m" }
|
|
152
|
+
)
|
|
153
|
+
return false if input.nil?
|
|
154
|
+
|
|
155
|
+
name = self.class.archive_name(input, plan.default_name)
|
|
156
|
+
zip_path = name && File.join(plan.dest_dir, name)
|
|
157
|
+
if zip_path.nil? || File.exist?(zip_path)
|
|
158
|
+
message = zip_path ? "Already exists: #{name}" : "Invalid archive name: #{input}"
|
|
159
|
+
@notification_manager&.add(message, :error, duration: 3, exit_code: nil)
|
|
160
|
+
return false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
start_compress(zip_path, plan.source_dir, plan.items)
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def dialog_options(lines)
|
|
168
|
+
{
|
|
169
|
+
width: DIALOG_WIDTH,
|
|
170
|
+
height: DIALOG_BORDER_HEIGHT + lines.length,
|
|
171
|
+
border_color: "\e[36m",
|
|
172
|
+
title_color: "\e[1;36m",
|
|
173
|
+
content_color: "\e[37m"
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# refresh_dirs: 完了時に表示中なら再読み込みするディレクトリ
|
|
178
|
+
def run_job(name, refresh_dirs)
|
|
179
|
+
job = @job_manager.add_job(name: name, path: refresh_dirs.first, command: nil)
|
|
180
|
+
job.start
|
|
181
|
+
last_percent = -PROGRESS_STEP
|
|
182
|
+
progress = lambda do |done, total|
|
|
183
|
+
percent = total.zero? ? 100 : (done * 100 / total)
|
|
184
|
+
next if percent < last_percent + PROGRESS_STEP
|
|
185
|
+
|
|
186
|
+
last_percent = percent
|
|
187
|
+
job.name = "#{name} #{percent}%"
|
|
188
|
+
@mutex.synchronize { @progress_changed = true }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
Thread.new do
|
|
192
|
+
begin
|
|
193
|
+
yield job, progress
|
|
194
|
+
job.name = name
|
|
195
|
+
job.complete(exit_code: 0) unless job.cancelled?
|
|
196
|
+
rescue ZipExtractor::CancelledError, ZipWriter::CancelledError
|
|
197
|
+
job.name = name
|
|
198
|
+
job.append_log('Cancelled')
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
job.name = name
|
|
201
|
+
job.append_log("Error: #{e.message}")
|
|
202
|
+
job.fail(exit_code: 1) unless job.cancelled?
|
|
203
|
+
ensure
|
|
204
|
+
@job_manager.notify_completion(job) unless job.cancelled?
|
|
205
|
+
@mutex.synchronize { @finished_dirs.concat(refresh_dirs) }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
job
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -135,6 +135,15 @@ module Rufio
|
|
|
135
135
|
@completion_message
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
# 完了メッセージを一度だけ受け取る。実行中は消費しない。
|
|
139
|
+
def consume_completion_message
|
|
140
|
+
return nil if running?
|
|
141
|
+
|
|
142
|
+
message = @completion_message
|
|
143
|
+
@completion_message = nil
|
|
144
|
+
message
|
|
145
|
+
end
|
|
146
|
+
|
|
138
147
|
# 現在実行中のコマンド名を取得
|
|
139
148
|
# @return [String, nil] コマンド名(実行中でない場合はnil)
|
|
140
149
|
def current_command
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# 完了通知とフッターランプで共有する実行状態・表示期限。
|
|
5
|
+
# 描画側は message を読むだけにし、状態の更新はメインループで行う。
|
|
6
|
+
class BackgroundStatus
|
|
7
|
+
POLL_INTERVAL = 0.1
|
|
8
|
+
DISPLAY_DURATION = 3.0
|
|
9
|
+
|
|
10
|
+
attr_reader :message
|
|
11
|
+
|
|
12
|
+
def initialize(now: Time.now)
|
|
13
|
+
@last_poll = now
|
|
14
|
+
@running = false
|
|
15
|
+
@message = nil
|
|
16
|
+
@expires_at = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 描画に関わる状態が変化した場合だけ true を返す。
|
|
20
|
+
def update(executor, now: Time.now)
|
|
21
|
+
changed = expire(now)
|
|
22
|
+
running = executor ? executor.running? : false
|
|
23
|
+
if running != @running
|
|
24
|
+
@running = running
|
|
25
|
+
changed = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if executor && now - @last_poll > POLL_INTERVAL
|
|
29
|
+
@last_poll = now
|
|
30
|
+
if (message = executor.consume_completion_message)
|
|
31
|
+
@message = message
|
|
32
|
+
@expires_at = now + DISPLAY_DURATION
|
|
33
|
+
changed = true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
changed
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def expire(now)
|
|
42
|
+
return false unless @expires_at && now >= @expires_at
|
|
43
|
+
|
|
44
|
+
@message = nil
|
|
45
|
+
@expires_at = nil
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'dialog_support'
|
|
4
|
+
|
|
3
5
|
module Rufio
|
|
4
6
|
# ブックマーク・zoxide・スクリプトパス専用コントローラ
|
|
5
7
|
# KeybindHandler からブックマーク系・zoxide・スクリプトパス系メソッドを分離し、単一責任原則に準拠
|
|
6
8
|
class BookmarkController
|
|
9
|
+
include DialogSupport
|
|
10
|
+
|
|
7
11
|
def initialize(directory_listing, bookmark_manager, dialog_renderer, nav_controller,
|
|
8
12
|
script_path_manager, notification_manager, zoxide_integration)
|
|
9
13
|
@directory_listing = directory_listing
|
|
@@ -78,8 +82,6 @@ module Rufio
|
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
def show_bookmark_menu
|
|
81
|
-
current_path = @directory_listing&.current_path || Dir.pwd
|
|
82
|
-
|
|
83
85
|
menu_items = [
|
|
84
86
|
'1. Add current dir to bookmarks',
|
|
85
87
|
'2. Add to script paths',
|
|
@@ -154,56 +156,14 @@ module Rufio
|
|
|
154
156
|
# 存在しないパスのブックマークはスキップする
|
|
155
157
|
# @return [Integer, nil] 新しいブックマークインデックス、またはnil
|
|
156
158
|
def goto_next_bookmark
|
|
157
|
-
|
|
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
|
|
159
|
+
cycle_bookmark(1)
|
|
179
160
|
end
|
|
180
161
|
|
|
181
162
|
# 前のブックマークに移動(Shift+Tabキー)
|
|
182
163
|
# 存在しないパスのブックマークはスキップする
|
|
183
164
|
# @return [Integer, nil] 新しいブックマークインデックス、またはnil
|
|
184
165
|
def goto_prev_bookmark
|
|
185
|
-
|
|
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
|
|
166
|
+
cycle_bookmark(-1)
|
|
207
167
|
end
|
|
208
168
|
|
|
209
169
|
# ============================
|
|
@@ -262,8 +222,6 @@ module Rufio
|
|
|
262
222
|
end
|
|
263
223
|
|
|
264
224
|
selected_index = 0
|
|
265
|
-
screen = @terminal_ui&.screen
|
|
266
|
-
renderer = @terminal_ui&.renderer
|
|
267
225
|
|
|
268
226
|
loop do
|
|
269
227
|
menu_items = paths.each_with_index.map do |path, i|
|
|
@@ -277,30 +235,13 @@ module Rufio
|
|
|
277
235
|
width = 50
|
|
278
236
|
height = content_lines.length + 4
|
|
279
237
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
238
|
+
key = show_overlay_dialog(title, content_lines, {
|
|
239
|
+
width: width,
|
|
240
|
+
height: height,
|
|
241
|
+
border_color: "\e[35m",
|
|
242
|
+
title_color: "\e[1;35m",
|
|
243
|
+
content_color: "\e[37m"
|
|
244
|
+
})
|
|
304
245
|
|
|
305
246
|
case key
|
|
306
247
|
when 'j'
|
|
@@ -358,59 +299,38 @@ module Rufio
|
|
|
358
299
|
# ============================
|
|
359
300
|
|
|
360
301
|
def navigate_to_directory(path)
|
|
361
|
-
|
|
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
|
|
302
|
+
@nav_controller.navigate_to_directory(path)
|
|
371
303
|
end
|
|
372
304
|
|
|
373
305
|
private
|
|
374
306
|
|
|
375
|
-
def
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
307
|
+
def cycle_bookmark(step)
|
|
308
|
+
bookmarks = @bookmark_manager.list
|
|
309
|
+
return nil unless bookmarks&.any?
|
|
405
310
|
|
|
406
|
-
|
|
407
|
-
|
|
311
|
+
len = bookmarks.length
|
|
312
|
+
current_path = @directory_listing.current_path
|
|
313
|
+
current_position = bookmarks.find_index { |bm| bm[:path] == current_path }
|
|
314
|
+
start_idx = if current_position
|
|
315
|
+
(current_position + step + len) % len
|
|
316
|
+
elsif @last_bookmark_idx
|
|
317
|
+
(@last_bookmark_idx + step + len) % len
|
|
318
|
+
else
|
|
319
|
+
step > 0 ? 0 : len - 1
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
len.times do |i|
|
|
323
|
+
idx = (start_idx + (i * step) + len) % len
|
|
324
|
+
next unless Dir.exist?(bookmarks[idx][:path])
|
|
408
325
|
|
|
409
|
-
|
|
326
|
+
@last_bookmark_idx = idx
|
|
327
|
+
navigate_to_directory(bookmarks[idx][:path])
|
|
328
|
+
return @last_bookmark_idx
|
|
410
329
|
end
|
|
330
|
+
nil
|
|
411
331
|
end
|
|
412
332
|
|
|
413
|
-
def show_notification(title,
|
|
333
|
+
def show_notification(title, _message, type)
|
|
414
334
|
return unless @notification_manager
|
|
415
335
|
|
|
416
336
|
@notification_manager.add(title, type, duration: 3, exit_code: nil)
|
|
@@ -424,16 +344,13 @@ module Rufio
|
|
|
424
344
|
|
|
425
345
|
def wait_for_keypress
|
|
426
346
|
print ConfigLoader.message('keybind.press_any_key') || 'Press any key to continue...'
|
|
427
|
-
|
|
347
|
+
KeyInput.getch
|
|
428
348
|
end
|
|
429
349
|
|
|
430
350
|
def truncate_path(path, max_length)
|
|
431
351
|
return path if path.length <= max_length
|
|
432
352
|
|
|
433
|
-
|
|
434
|
-
return display_path if display_path.length <= max_length
|
|
435
|
-
|
|
436
|
-
"...#{display_path[-(max_length - 3)..]}"
|
|
353
|
+
TextUtils.shorten_path_from_end(path, max_length, tilde: true)
|
|
437
354
|
end
|
|
438
355
|
end
|
|
439
356
|
end
|