rufio 1.1.0 → 1.4.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 +50 -0
- data/README.md +26 -297
- data/README_ja.md +24 -261
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/docs/guide.md +410 -0
- data/docs/guide_ja.md +410 -0
- data/info/help.md +54 -17
- data/info/keybindings.md +50 -22
- data/info/welcome.md +14 -5
- 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 +28 -2
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +60 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +4 -11
- data/lib/rufio/dialog_renderer.rb +90 -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_operations.rb +28 -6
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +125 -13
- data/lib/rufio/help_menu.rb +19 -7
- 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 +83 -116
- 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 +20 -26
- data/lib/rufio/path_jump.rb +55 -0
- 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 +84 -298
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +73 -45
- 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 +6 -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 +128 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
data/lib/rufio/terminal_ui.rb
CHANGED
|
@@ -4,39 +4,18 @@ require 'io/console'
|
|
|
4
4
|
require_relative 'text_utils'
|
|
5
5
|
require_relative 'help_menu'
|
|
6
6
|
require_relative 'global_search'
|
|
7
|
+
require_relative 'ui_renderer'
|
|
7
8
|
|
|
8
9
|
module Rufio
|
|
9
10
|
class TerminalUI
|
|
10
|
-
# Layout constants
|
|
11
|
-
HEADER_HEIGHT = 1 # Header占有行数(モードタブ+パス+バージョン 1行に統合)
|
|
12
|
-
FOOTER_HEIGHT = 1 # Footer占有行数(ブックマーク一覧 + ステータス情報)
|
|
13
|
-
HEADER_FOOTER_MARGIN = 2 # Header(1行) + Footer(1行)分のマージン
|
|
14
|
-
|
|
15
|
-
# Panel layout ratios
|
|
16
|
-
LEFT_PANEL_RATIO = 0.5 # 左パネルの幅比率
|
|
17
|
-
RIGHT_PANEL_RATIO = 1.0 - LEFT_PANEL_RATIO
|
|
18
|
-
|
|
19
11
|
# Display constants
|
|
20
12
|
DEFAULT_SCREEN_WIDTH = 80 # デフォルト画面幅
|
|
21
13
|
DEFAULT_SCREEN_HEIGHT = 24 # デフォルト画面高さ
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
TAB_SEPARATOR = ">" # タブ間セパレータ
|
|
25
|
-
|
|
26
|
-
# File display constants
|
|
27
|
-
ICON_SIZE_PADDING = 12 # アイコン、選択マーク、サイズ情報分
|
|
28
|
-
CURSOR_OFFSET = 1 # カーソル位置のオフセット
|
|
29
|
-
|
|
30
|
-
# Size display constants (bytes)
|
|
31
|
-
KILOBYTE = 1024
|
|
32
|
-
MEGABYTE = KILOBYTE * 1024
|
|
33
|
-
GIGABYTE = MEGABYTE * 1024
|
|
14
|
+
HEADER_FOOTER_MARGIN = UIRenderer::HEADER_FOOTER_MARGIN
|
|
15
|
+
CONTENT_START_LINE = UIRenderer::CONTENT_START_LINE
|
|
34
16
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# Line offsets
|
|
39
|
-
CONTENT_START_LINE = 1 # コンテンツ開始行(フッタ1行: Y=0)
|
|
17
|
+
# T キーで終了した時のディレクトリ(それ以外の終了では nil)
|
|
18
|
+
attr_reader :exit_directory
|
|
40
19
|
|
|
41
20
|
def initialize(test_mode: false)
|
|
42
21
|
console = IO.console
|
|
@@ -73,17 +52,6 @@ module Rufio
|
|
|
73
52
|
@in_job_mode = false
|
|
74
53
|
@job_mode_needs_redraw = false
|
|
75
54
|
|
|
76
|
-
# Preview cache
|
|
77
|
-
@preview_cache = {}
|
|
78
|
-
@last_preview_path = nil
|
|
79
|
-
|
|
80
|
-
# シンタックスハイライター(bat が利用可能な場合のみ動作)
|
|
81
|
-
@syntax_highlighter = SyntaxHighlighter.new
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# Tab mode manager
|
|
85
|
-
@tab_mode_manager = TabModeManager.new
|
|
86
|
-
|
|
87
55
|
# UIRenderer(描画ロジックを担当)
|
|
88
56
|
ui_opts = ConfigLoader.ui_options
|
|
89
57
|
@ui_renderer = UIRenderer.new(
|
|
@@ -93,9 +61,11 @@ module Rufio
|
|
|
93
61
|
left_panel_ratio: ui_opts[:panel_ratio],
|
|
94
62
|
preview_enabled: ui_opts[:preview_enabled]
|
|
95
63
|
)
|
|
64
|
+
@tab_mode_manager = @ui_renderer.tab_mode_manager
|
|
65
|
+
@background_status = @ui_renderer.background_status
|
|
96
66
|
end
|
|
97
67
|
|
|
98
|
-
attr_reader :ui_renderer
|
|
68
|
+
attr_reader :ui_renderer, :screen_width, :screen_height
|
|
99
69
|
|
|
100
70
|
def start(directory_listing, keybind_handler, file_preview, background_executor = nil)
|
|
101
71
|
@directory_listing = directory_listing
|
|
@@ -137,24 +107,10 @@ module Rufio
|
|
|
137
107
|
# ウィンドウサイズを更新してから画面をクリアして再描画
|
|
138
108
|
update_screen_size
|
|
139
109
|
|
|
140
|
-
|
|
141
|
-
@
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# ブックマークキャッシュもクリア
|
|
145
|
-
@cached_bookmarks = nil
|
|
146
|
-
@cached_bookmark_time = nil
|
|
147
|
-
|
|
148
|
-
# バッファベースの描画が利用可能な場合は全画面を再描画
|
|
149
|
-
if @screen && @renderer
|
|
150
|
-
# レンダラーの前フレーム情報をリセット(差分レンダリングを強制的に全体描画にする)
|
|
151
|
-
@renderer.clear
|
|
152
|
-
@screen.clear
|
|
153
|
-
draw_screen_to_buffer(@screen, nil, nil)
|
|
154
|
-
@renderer.render(@screen)
|
|
155
|
-
# カーソルを画面外に移動
|
|
156
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
157
|
-
end
|
|
110
|
+
@ui_renderer.clear_preview_cache
|
|
111
|
+
@ui_renderer.clear_bookmark_cache
|
|
112
|
+
|
|
113
|
+
render_full_screen
|
|
158
114
|
end
|
|
159
115
|
|
|
160
116
|
# スクリプトランナーを設定
|
|
@@ -207,6 +163,8 @@ module Rufio
|
|
|
207
163
|
rescue
|
|
208
164
|
# スレッド終了
|
|
209
165
|
end
|
|
166
|
+
# ダイアログのキー入力もこのキューから読ませ、入力スレッドとの奪い合いを防ぐ
|
|
167
|
+
KeyInput.attach(@input_queue)
|
|
210
168
|
end
|
|
211
169
|
|
|
212
170
|
# SGR拡張マウスレポートを有効化
|
|
@@ -238,11 +196,14 @@ module Rufio
|
|
|
238
196
|
end
|
|
239
197
|
|
|
240
198
|
# エスケープシーケンスの後続バイトを読み取る(矢印キー等の短いシーケンス用)。
|
|
241
|
-
# Windows: IO.select
|
|
199
|
+
# Windows: IO.select を使わずキューから取得。
|
|
242
200
|
def read_next_input_byte
|
|
243
201
|
if windows?
|
|
244
|
-
# ConPTY ではバイト到着が遅れることがあるため 5ms では ESC
|
|
245
|
-
|
|
202
|
+
# ConPTY ではバイト到着が遅れることがあるため 5ms では ESC 単体と誤判定する。
|
|
203
|
+
# G mode は検索中に全画面を再描画し続けて遅延が大きくなりやすく、
|
|
204
|
+
# しかも ESC 単体で閉じてしまうため、待ち時間を長めに取る。
|
|
205
|
+
timeout = global_search_active? ? 0.1 : 0.025
|
|
206
|
+
b = windows_queue_pop_with_timeout(timeout)
|
|
246
207
|
b.nil? ? nil : b.chr(Encoding::BINARY)
|
|
247
208
|
else
|
|
248
209
|
STDIN.read_nonblock(1) rescue nil
|
|
@@ -263,46 +224,12 @@ module Rufio
|
|
|
263
224
|
# Windows 専用: キューから1文字(マルチバイト対応)を組み立てる。
|
|
264
225
|
# first_byte_int は getbyte が返す Integer。
|
|
265
226
|
def windows_build_char(first_byte_int)
|
|
266
|
-
|
|
267
|
-
when 0x00..0x7F then 0
|
|
268
|
-
when 0xC0..0xDF then 1
|
|
269
|
-
when 0xE0..0xEF then 2
|
|
270
|
-
when 0xF0..0xF7 then 3
|
|
271
|
-
else 0
|
|
272
|
-
end
|
|
273
|
-
if remaining == 0
|
|
274
|
-
first_byte_int.chr(Encoding::UTF_8)
|
|
275
|
-
else
|
|
276
|
-
buf = first_byte_int.chr(Encoding::BINARY)
|
|
277
|
-
remaining.times do
|
|
278
|
-
b = windows_queue_pop_with_timeout(0.005)
|
|
279
|
-
return nil if b.nil?
|
|
280
|
-
buf << b.chr(Encoding::BINARY)
|
|
281
|
-
end
|
|
282
|
-
result = buf.force_encoding(Encoding::UTF_8)
|
|
283
|
-
result.valid_encoding? ? result : nil
|
|
284
|
-
end
|
|
227
|
+
KeyInput.build_char(first_byte_int, @input_queue)
|
|
285
228
|
end
|
|
286
229
|
|
|
287
230
|
# Windows 専用: @input_queue からタイムアウト付きで Integer バイトを取り出す。
|
|
288
|
-
# Ruby 3.2+ は Queue#pop(timeout:) で正確に待機する(sleep ポーリングは
|
|
289
|
-
# Windows のタイマー分解能 約15ms により遅延する)。それ以前はポーリング。
|
|
290
|
-
QUEUE_POP_TIMEOUT_SUPPORTED = Queue.instance_method(:pop).parameters.any? { |_, n| n == :timeout }
|
|
291
|
-
|
|
292
231
|
def windows_queue_pop_with_timeout(timeout_sec)
|
|
293
|
-
|
|
294
|
-
return @input_queue.pop(timeout: timeout_sec)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
deadline = Time.now + timeout_sec
|
|
298
|
-
loop do
|
|
299
|
-
begin
|
|
300
|
-
return @input_queue.pop(true)
|
|
301
|
-
rescue ThreadError
|
|
302
|
-
return nil if Time.now >= deadline
|
|
303
|
-
sleep 0.001
|
|
304
|
-
end
|
|
305
|
-
end
|
|
232
|
+
KeyInput.pop_with_timeout(@input_queue, timeout_sec)
|
|
306
233
|
end
|
|
307
234
|
|
|
308
235
|
def cleanup_terminal
|
|
@@ -317,6 +244,7 @@ module Rufio
|
|
|
317
244
|
|
|
318
245
|
# Windows 入力スレッドを停止
|
|
319
246
|
if windows? && @input_thread
|
|
247
|
+
KeyInput.detach
|
|
320
248
|
@stop_input_thread = true
|
|
321
249
|
@input_thread.kill rescue nil
|
|
322
250
|
@input_thread.join(0.5) rescue nil
|
|
@@ -365,7 +293,6 @@ module Rufio
|
|
|
365
293
|
# 最小スリープ時間(入力チェック間隔)
|
|
366
294
|
# Windows は入力をキュー経由で受けるため、入力遅延を抑えるよう短めにする
|
|
367
295
|
min_sleep_interval = windows? ? 0.008 : 0.0333 # Windows: 約8ms / Unix: 30FPS
|
|
368
|
-
check_interval = 0.1 # バックグラウンドタスクのチェック間隔
|
|
369
296
|
|
|
370
297
|
# Phase 3: Screen/Rendererを初期化
|
|
371
298
|
@screen = Screen.new(@screen_width, @screen_height)
|
|
@@ -376,13 +303,6 @@ module Rufio
|
|
|
376
303
|
draw_screen_to_buffer(@screen, nil, nil)
|
|
377
304
|
@renderer.render(@screen)
|
|
378
305
|
|
|
379
|
-
last_notification_check = Time.now
|
|
380
|
-
last_lamp_check = Time.now
|
|
381
|
-
notification_message = nil
|
|
382
|
-
notification_time = nil
|
|
383
|
-
previous_notification = nil
|
|
384
|
-
previous_lamp_message = @ui_renderer.completion_lamp_message
|
|
385
|
-
|
|
386
306
|
# FPS計測用
|
|
387
307
|
frame_times = []
|
|
388
308
|
last_frame_time = Time.now
|
|
@@ -429,56 +349,11 @@ module Rufio
|
|
|
429
349
|
end
|
|
430
350
|
needs_redraw = true if had_input
|
|
431
351
|
|
|
432
|
-
#
|
|
433
|
-
if @background_executor
|
|
434
|
-
if !@background_executor.running? && @background_executor.get_completion_message
|
|
435
|
-
completion_msg = @background_executor.get_completion_message
|
|
436
|
-
# 通知メッセージとして表示
|
|
437
|
-
notification_message = completion_msg
|
|
438
|
-
notification_time = start
|
|
439
|
-
# フッターのランプ表示用にも設定(UIRenderer が管理)
|
|
440
|
-
@ui_renderer.completion_lamp_message = completion_msg
|
|
441
|
-
@ui_renderer.completion_lamp_time = start
|
|
442
|
-
@background_executor.instance_variable_set(:@completion_message, nil) # メッセージをクリア
|
|
443
|
-
needs_redraw = true
|
|
444
|
-
end
|
|
445
|
-
last_notification_check = start
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
# バックグラウンドコマンドの実行状態が変わった場合も再描画
|
|
449
|
-
if @background_executor
|
|
450
|
-
current_running = @background_executor.running?
|
|
451
|
-
if @last_bg_running != current_running
|
|
452
|
-
@last_bg_running = current_running
|
|
453
|
-
needs_redraw = true
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
# 完了ランプの表示状態をチェック(0.5秒ごと)
|
|
458
|
-
if (start - last_lamp_check) > 0.5
|
|
459
|
-
current_lamp = @ui_renderer.completion_lamp_message
|
|
460
|
-
if current_lamp != previous_lamp_message
|
|
461
|
-
previous_lamp_message = current_lamp
|
|
462
|
-
needs_redraw = true
|
|
463
|
-
end
|
|
464
|
-
# 完了ランプのタイムアウトチェック
|
|
465
|
-
if @ui_renderer.completion_lamp_message && @ui_renderer.completion_lamp_time &&
|
|
466
|
-
(start - @ui_renderer.completion_lamp_time) >= 3.0
|
|
467
|
-
@ui_renderer.completion_lamp_message = nil
|
|
468
|
-
needs_redraw = true
|
|
469
|
-
end
|
|
470
|
-
last_lamp_check = start
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
# 通知メッセージの変化をチェック
|
|
474
|
-
current_notification = notification_message && (start - notification_time) < 3.0 ? notification_message : nil
|
|
475
|
-
if current_notification != previous_notification
|
|
476
|
-
previous_notification = current_notification
|
|
477
|
-
notification_message = nil if current_notification.nil?
|
|
478
|
-
needs_redraw = true
|
|
479
|
-
end
|
|
352
|
+
# 通知・ランプ・実行状態の更新を一か所に集約する。
|
|
353
|
+
needs_redraw = true if @background_status.update(@background_executor, now: start)
|
|
480
354
|
|
|
481
355
|
needs_redraw = true if @global_search.poll
|
|
356
|
+
needs_redraw = true if @keybind_handler&.poll_archive_jobs
|
|
482
357
|
|
|
483
358
|
# 非同期シンタックスハイライト完了チェック(UIRenderer側のフラグを参照)
|
|
484
359
|
if @ui_renderer.highlight_updated?
|
|
@@ -495,11 +370,7 @@ module Rufio
|
|
|
495
370
|
# DRAW & RENDER phase - 変更があった場合のみ描画
|
|
496
371
|
if needs_redraw
|
|
497
372
|
# Screenバッファに描画(clearは呼ばない。必要な部分だけ更新)
|
|
498
|
-
|
|
499
|
-
draw_screen_to_buffer(@screen, notification_message, current_fps)
|
|
500
|
-
else
|
|
501
|
-
draw_screen_to_buffer(@screen, nil, current_fps)
|
|
502
|
-
end
|
|
373
|
+
draw_screen_to_buffer(@screen, @background_status.message, current_fps)
|
|
503
374
|
|
|
504
375
|
# ヘルプまたはコマンドモードのダイアログをオーバーレイに描画
|
|
505
376
|
if help_dialog_active?
|
|
@@ -668,6 +539,12 @@ module Rufio
|
|
|
668
539
|
@running = false
|
|
669
540
|
end
|
|
670
541
|
|
|
542
|
+
# T キー: 確認なしで終了し、現在のディレクトリを終了後の移動先にする
|
|
543
|
+
if result == :quit_cd
|
|
544
|
+
@exit_directory = @directory_listing.current_path
|
|
545
|
+
@running = false
|
|
546
|
+
end
|
|
547
|
+
|
|
671
548
|
# 入力があったことを返す
|
|
672
549
|
true
|
|
673
550
|
end
|
|
@@ -740,6 +617,8 @@ module Rufio
|
|
|
740
617
|
|
|
741
618
|
# マウス左クリックを処理する
|
|
742
619
|
def handle_mouse_left_click(col, row)
|
|
620
|
+
return if @keybind_handler.preview_focused?
|
|
621
|
+
|
|
743
622
|
left_width = left_panel_col_width
|
|
744
623
|
|
|
745
624
|
if col <= left_width
|
|
@@ -757,13 +636,14 @@ module Rufio
|
|
|
757
636
|
|
|
758
637
|
# ファイルリストのクリック行からエントリを選択する(ダブルクリック対応)
|
|
759
638
|
def handle_mouse_file_click(row)
|
|
760
|
-
#
|
|
761
|
-
|
|
762
|
-
|
|
639
|
+
# コンテンツ行はターミナル行(1 始まり)で CONTENT_START_LINE+1 〜 screen_height-1
|
|
640
|
+
first_row = CONTENT_START_LINE + 1
|
|
641
|
+
content_height = @screen_height - HEADER_FOOTER_MARGIN
|
|
642
|
+
return unless row >= first_row && row <= @screen_height - 1
|
|
763
643
|
|
|
764
644
|
current_idx = @keybind_handler.current_index
|
|
765
645
|
start_index = [current_idx - content_height / 2, 0].max
|
|
766
|
-
target_index = start_index + (row -
|
|
646
|
+
target_index = start_index + (row - first_row)
|
|
767
647
|
|
|
768
648
|
entries = @keybind_handler.send(:get_active_entries)
|
|
769
649
|
return unless target_index >= 0 && target_index < entries.length
|
|
@@ -784,10 +664,8 @@ module Rufio
|
|
|
784
664
|
end
|
|
785
665
|
|
|
786
666
|
# マウスホイールスクロールを処理する
|
|
787
|
-
def handle_mouse_scroll(direction,
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if col > left_width && @keybind_handler.preview_focused?
|
|
667
|
+
def handle_mouse_scroll(direction, _col, _row)
|
|
668
|
+
if @keybind_handler.preview_focused?
|
|
791
669
|
# プレビューペインのスクロール
|
|
792
670
|
case direction
|
|
793
671
|
when :up then @keybind_handler.scroll_preview_up
|
|
@@ -809,37 +687,7 @@ module Rufio
|
|
|
809
687
|
|
|
810
688
|
# モード変更を適用
|
|
811
689
|
def apply_mode_change(mode)
|
|
812
|
-
|
|
813
|
-
when :config
|
|
814
|
-
@keybind_handler.enter_config_mode
|
|
815
|
-
when :global
|
|
816
|
-
@keybind_handler.start_global_search
|
|
817
|
-
when :files
|
|
818
|
-
# ヘルプモードまたはログビューワモードから戻る
|
|
819
|
-
if @keybind_handler.help_mode?
|
|
820
|
-
@keybind_handler.send(:exit_help_mode)
|
|
821
|
-
elsif @keybind_handler.log_viewer_mode?
|
|
822
|
-
@keybind_handler.send(:exit_log_viewer_mode)
|
|
823
|
-
elsif @keybind_handler.in_job_mode?
|
|
824
|
-
@keybind_handler.send(:exit_job_mode)
|
|
825
|
-
end
|
|
826
|
-
when :help
|
|
827
|
-
show_help_dialog
|
|
828
|
-
when :logs
|
|
829
|
-
# ログビューワモードに入る
|
|
830
|
-
unless @keybind_handler.log_viewer_mode?
|
|
831
|
-
@keybind_handler.send(:exit_help_mode) if @keybind_handler.help_mode?
|
|
832
|
-
@keybind_handler.send(:exit_job_mode) if @keybind_handler.in_job_mode?
|
|
833
|
-
@keybind_handler.send(:enter_log_viewer_mode)
|
|
834
|
-
end
|
|
835
|
-
when :jobs
|
|
836
|
-
# ジョブモードに入る
|
|
837
|
-
unless @keybind_handler.in_job_mode?
|
|
838
|
-
@keybind_handler.send(:exit_help_mode) if @keybind_handler.help_mode?
|
|
839
|
-
@keybind_handler.send(:exit_log_viewer_mode) if @keybind_handler.log_viewer_mode?
|
|
840
|
-
@keybind_handler.enter_job_mode
|
|
841
|
-
end
|
|
842
|
-
end
|
|
690
|
+
@keybind_handler.switch_mode(mode)
|
|
843
691
|
end
|
|
844
692
|
|
|
845
693
|
# コマンドモード関連のメソッドは public にする
|
|
@@ -915,22 +763,9 @@ module Rufio
|
|
|
915
763
|
@command_input = ""
|
|
916
764
|
when "\e"
|
|
917
765
|
# Escape キーでコマンドモードをキャンセル
|
|
918
|
-
# まずコマンドウィンドウをクリア
|
|
919
766
|
@command_mode_ui.clear_prompt
|
|
920
767
|
deactivate_command_mode
|
|
921
|
-
|
|
922
|
-
if @screen && @renderer
|
|
923
|
-
# レンダラーの前フレーム情報をリセット(差分レンダリングを強制的に全体描画にする)
|
|
924
|
-
@renderer.clear
|
|
925
|
-
@screen.clear
|
|
926
|
-
draw_screen_to_buffer(@screen, nil, nil)
|
|
927
|
-
@renderer.render(@screen)
|
|
928
|
-
# カーソルを画面外に移動(メインループと同じ処理)
|
|
929
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
930
|
-
else
|
|
931
|
-
# フォールバック(古い実装)
|
|
932
|
-
draw_screen
|
|
933
|
-
end
|
|
768
|
+
render_full_screen
|
|
934
769
|
when "\t"
|
|
935
770
|
# Tab キーで補完
|
|
936
771
|
handle_tab_completion
|
|
@@ -955,6 +790,9 @@ module Rufio
|
|
|
955
790
|
|
|
956
791
|
result = @command_mode.execute(command_string, working_dir: working_dir)
|
|
957
792
|
|
|
793
|
+
# :cd はディレクトリを移動してコマンドモードを閉じる
|
|
794
|
+
return change_directory(result[:cd]) if result.is_a?(Hash) && result.key?(:cd)
|
|
795
|
+
|
|
958
796
|
# バックグラウンドコマンドの場合は結果表示をスキップ
|
|
959
797
|
# (完了通知は別途メインループで表示される)
|
|
960
798
|
if result && !result.to_s.include?("🔄 Running in background")
|
|
@@ -967,6 +805,28 @@ module Rufio
|
|
|
967
805
|
# 使用するとコマンドプロンプトの枠線が残る不具合が発生する
|
|
968
806
|
end
|
|
969
807
|
|
|
808
|
+
# :cd の移動先へ移動する。ファイルが指定されていればそのファイルを選択する
|
|
809
|
+
# @param target [Hash, Symbol] { directory:, file: } または :previous(cd -)
|
|
810
|
+
def change_directory(target)
|
|
811
|
+
if target == :previous
|
|
812
|
+
return @command_mode_ui.show_result('⚠️ No previous directory') unless @previous_directory
|
|
813
|
+
|
|
814
|
+
target = { directory: @previous_directory, file: nil }
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
origin = @directory_listing&.current_path
|
|
818
|
+
unless @keybind_handler.navigate_to_directory(target[:directory])
|
|
819
|
+
return @command_mode_ui.show_result("⚠️ Cannot open directory: #{target[:directory]}")
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
@previous_directory = origin
|
|
823
|
+
index = target[:file] && @keybind_handler.get_active_entries.index { |entry| entry[:path] == target[:file] }
|
|
824
|
+
@keybind_handler.select_index(index || 0)
|
|
825
|
+
@keybind_handler.unfocus_preview_pane
|
|
826
|
+
@tab_mode_manager&.switch_to(@keybind_handler.config_mode? ? :config : :files)
|
|
827
|
+
deactivate_command_mode
|
|
828
|
+
end
|
|
829
|
+
|
|
970
830
|
# Tab補完を処理
|
|
971
831
|
def handle_tab_completion
|
|
972
832
|
# 補完候補を取得
|
|
@@ -1057,16 +917,6 @@ module Rufio
|
|
|
1057
917
|
info_notice.mark_as_shown(notice[:file])
|
|
1058
918
|
end
|
|
1059
919
|
|
|
1060
|
-
# ログモードに入る(廃止済み: 空のメソッド)
|
|
1061
|
-
def enter_log_mode(_project_log)
|
|
1062
|
-
# プロジェクトモード廃止により何もしない
|
|
1063
|
-
end
|
|
1064
|
-
|
|
1065
|
-
# ログモードを終了(廃止済み: 空のメソッド)
|
|
1066
|
-
def exit_log_mode
|
|
1067
|
-
# プロジェクトモード廃止により何もしない
|
|
1068
|
-
end
|
|
1069
|
-
|
|
1070
920
|
# ジョブモードを設定
|
|
1071
921
|
def set_job_mode(job_mode, job_manager, notification_manager)
|
|
1072
922
|
@job_mode_instance = job_mode
|
|
@@ -1084,17 +934,8 @@ module Rufio
|
|
|
1084
934
|
@in_job_mode = false
|
|
1085
935
|
@job_mode_instance = nil
|
|
1086
936
|
@job_manager = nil
|
|
1087
|
-
# バッファベースの全画面再描画を使用
|
|
1088
937
|
update_screen_size
|
|
1089
|
-
|
|
1090
|
-
@renderer.clear
|
|
1091
|
-
@screen.clear
|
|
1092
|
-
draw_screen_to_buffer(@screen, nil, nil)
|
|
1093
|
-
@renderer.render(@screen)
|
|
1094
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
1095
|
-
else
|
|
1096
|
-
draw_screen
|
|
1097
|
-
end
|
|
938
|
+
render_full_screen
|
|
1098
939
|
end
|
|
1099
940
|
|
|
1100
941
|
# ジョブモード再描画をトリガー
|
|
@@ -1102,70 +943,6 @@ module Rufio
|
|
|
1102
943
|
@job_mode_needs_redraw = true
|
|
1103
944
|
end
|
|
1104
945
|
|
|
1105
|
-
# ジョブモード画面を描画(バッファベース描画への橋渡し)
|
|
1106
|
-
def draw_job_mode_screen
|
|
1107
|
-
return unless @in_job_mode && @job_mode_instance && @job_manager
|
|
1108
|
-
return unless @screen && @renderer
|
|
1109
|
-
|
|
1110
|
-
# バッファベースの描画を使用
|
|
1111
|
-
draw_screen_to_buffer(@screen, nil, nil)
|
|
1112
|
-
@renderer.render(@screen)
|
|
1113
|
-
print "\e[#{@screen_height};#{@screen_width}H"
|
|
1114
|
-
|
|
1115
|
-
STDOUT.flush
|
|
1116
|
-
@job_mode_needs_redraw = false
|
|
1117
|
-
end
|
|
1118
|
-
|
|
1119
|
-
# Noice風の通知を描画
|
|
1120
|
-
def draw_notifications
|
|
1121
|
-
nm = @notification_manager || @keybind_handler&.notification_manager
|
|
1122
|
-
return unless nm
|
|
1123
|
-
|
|
1124
|
-
# 期限切れの通知を削除
|
|
1125
|
-
nm.expire_old_notifications
|
|
1126
|
-
|
|
1127
|
-
notifications = nm.notifications
|
|
1128
|
-
return if notifications.empty?
|
|
1129
|
-
|
|
1130
|
-
# 通知の幅と位置
|
|
1131
|
-
notification_width = 22
|
|
1132
|
-
x = @screen_width - notification_width - 2 # 右端から2文字マージン
|
|
1133
|
-
|
|
1134
|
-
notifications.each_with_index do |notif, i|
|
|
1135
|
-
y = 2 + (i * 5) # 各通知4行 + 間隔1行
|
|
1136
|
-
|
|
1137
|
-
# 色設定
|
|
1138
|
-
border_color = notif[:border_color] == :green ? "\e[32m" : "\e[31m"
|
|
1139
|
-
reset = "\e[0m"
|
|
1140
|
-
|
|
1141
|
-
# ステータスアイコン
|
|
1142
|
-
icon = notif[:type] == :success ? '✓' : '✗'
|
|
1143
|
-
|
|
1144
|
-
# 通知の内容を作成
|
|
1145
|
-
name_line = "#{icon} #{notif[:name]}"[0...notification_width - 4]
|
|
1146
|
-
status_line = notif[:status_text][0...notification_width - 4]
|
|
1147
|
-
|
|
1148
|
-
# 上部ボーダー
|
|
1149
|
-
print "\e[#{y};#{x}H#{border_color}╭#{'─' * (notification_width - 2)}╮#{reset}"
|
|
1150
|
-
|
|
1151
|
-
# 1行目: アイコン + 名前
|
|
1152
|
-
print "\e[#{y + 1};#{x}H#{border_color}│#{reset} #{name_line.ljust(notification_width - 4)} #{border_color}│#{reset}"
|
|
1153
|
-
|
|
1154
|
-
# 2行目: ステータス
|
|
1155
|
-
print "\e[#{y + 2};#{x}H#{border_color}│#{reset} #{status_line.ljust(notification_width - 6)} #{border_color}│#{reset}"
|
|
1156
|
-
|
|
1157
|
-
# Exit code行(失敗時のみ)
|
|
1158
|
-
if notif[:type] == :error && notif[:exit_code]
|
|
1159
|
-
exit_line = "Exit code: #{notif[:exit_code]}"[0...notification_width - 6]
|
|
1160
|
-
print "\e[#{y + 3};#{x}H#{border_color}│#{reset} #{exit_line.ljust(notification_width - 6)} #{border_color}│#{reset}"
|
|
1161
|
-
print "\e[#{y + 4};#{x}H#{border_color}╰#{'─' * (notification_width - 2)}╯#{reset}"
|
|
1162
|
-
else
|
|
1163
|
-
# 下部ボーダー
|
|
1164
|
-
print "\e[#{y + 3};#{x}H#{border_color}╰#{'─' * (notification_width - 2)}╯#{reset}"
|
|
1165
|
-
end
|
|
1166
|
-
end
|
|
1167
|
-
end
|
|
1168
|
-
|
|
1169
946
|
# オーバーレイダイアログを表示してキー入力を待つヘルパーメソッド
|
|
1170
947
|
# @param title [String] ダイアログタイトル
|
|
1171
948
|
# @param content_lines [Array<String>] コンテンツ行
|
|
@@ -1212,7 +989,7 @@ module Rufio
|
|
|
1212
989
|
@renderer.render(@screen)
|
|
1213
990
|
|
|
1214
991
|
# キー入力を待つ
|
|
1215
|
-
key = block_given? ? yield :
|
|
992
|
+
key = block_given? ? yield : KeyInput.getch
|
|
1216
993
|
|
|
1217
994
|
# オーバーレイを無効化
|
|
1218
995
|
@screen.disable_overlay
|
|
@@ -1241,10 +1018,8 @@ module Rufio
|
|
|
1241
1018
|
end
|
|
1242
1019
|
result = @global_search.handle_key(input)
|
|
1243
1020
|
if result.is_a?(Hash) && File.file?(result[:path])
|
|
1244
|
-
@keybind_handler.
|
|
1245
|
-
|
|
1246
|
-
@keybind_handler.exit_job_mode if @keybind_handler.in_job_mode?
|
|
1247
|
-
if @keybind_handler.send(:navigate_to_directory, File.dirname(result[:path]))
|
|
1021
|
+
@keybind_handler.switch_mode(:files)
|
|
1022
|
+
if @keybind_handler.navigate_to_directory(File.dirname(result[:path]))
|
|
1248
1023
|
index = @keybind_handler.get_active_entries.index { |entry| entry[:path] == result[:path] }
|
|
1249
1024
|
@keybind_handler.select_index(index) if index
|
|
1250
1025
|
@keybind_handler.unfocus_preview_pane
|
|
@@ -1291,6 +1066,17 @@ module Rufio
|
|
|
1291
1066
|
end
|
|
1292
1067
|
end
|
|
1293
1068
|
|
|
1069
|
+
# 全画面をバッファから再描画する
|
|
1070
|
+
def render_full_screen
|
|
1071
|
+
return unless @screen && @renderer
|
|
1072
|
+
|
|
1073
|
+
@renderer.clear
|
|
1074
|
+
@screen.clear
|
|
1075
|
+
draw_screen_to_buffer(@screen, nil, nil)
|
|
1076
|
+
@renderer.render(@screen)
|
|
1077
|
+
print "\e[#{@screen_height};#{@screen_width}H"
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1294
1080
|
end
|
|
1295
1081
|
end
|
|
1296
1082
|
|
data/lib/rufio/text_utils.rb
CHANGED
|
@@ -134,7 +134,7 @@ module Rufio
|
|
|
134
134
|
line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
135
135
|
# Remove trailing whitespace
|
|
136
136
|
line = line.rstrip
|
|
137
|
-
rescue EncodingError, ArgumentError
|
|
137
|
+
rescue EncodingError, ArgumentError
|
|
138
138
|
# If encoding fails completely, skip this line
|
|
139
139
|
wrapped << '[encoding error]'
|
|
140
140
|
next
|
|
@@ -152,7 +152,7 @@ module Rufio
|
|
|
152
152
|
wrapped << line
|
|
153
153
|
next
|
|
154
154
|
end
|
|
155
|
-
rescue ArgumentError
|
|
155
|
+
rescue ArgumentError
|
|
156
156
|
# If display_width fails, just truncate by byte length
|
|
157
157
|
if line.bytesize <= max_width
|
|
158
158
|
wrapped << line
|
|
@@ -181,7 +181,7 @@ module Rufio
|
|
|
181
181
|
|
|
182
182
|
# Add remaining characters
|
|
183
183
|
wrapped << current_line.join unless current_line.empty?
|
|
184
|
-
rescue ArgumentError, EncodingError
|
|
184
|
+
rescue ArgumentError, EncodingError
|
|
185
185
|
# If character iteration fails, just add the line truncated
|
|
186
186
|
truncated = line.byteslice(0, [max_width, line.bytesize].min)
|
|
187
187
|
wrapped << (truncated || line)
|
|
@@ -190,5 +190,28 @@ module Rufio
|
|
|
190
190
|
|
|
191
191
|
wrapped
|
|
192
192
|
end
|
|
193
|
+
|
|
194
|
+
# Shorten a path by keeping the tail and prefixing with ellipsis if longer than max_length.
|
|
195
|
+
# Optionally replaces ENV['HOME'] / Dir.home with '~' when tilde: true.
|
|
196
|
+
def shorten_path_from_end(path, max_length, tilde: false, ellipsis: '...')
|
|
197
|
+
return path.to_s if max_length <= 0
|
|
198
|
+
|
|
199
|
+
display = path.to_s
|
|
200
|
+
if tilde
|
|
201
|
+
home = Dir.home rescue ENV['HOME']
|
|
202
|
+
if home && !home.empty?
|
|
203
|
+
if display == home
|
|
204
|
+
display = '~'
|
|
205
|
+
elsif display.start_with?(home + '/')
|
|
206
|
+
display = "~#{display.delete_prefix(home)}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
return display if display.length <= max_length
|
|
212
|
+
return display[-max_length..] || display if max_length <= ellipsis.length
|
|
213
|
+
|
|
214
|
+
"#{ellipsis}#{display[-(max_length - ellipsis.length)..]}"
|
|
215
|
+
end
|
|
193
216
|
end
|
|
194
217
|
end
|