rufio 0.20.0 → 0.30.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.
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'io/console'
4
+ require_relative 'text_utils'
4
5
 
5
6
  module Rufio
6
7
  class TerminalUI
7
8
  # Layout constants
8
- HEADER_HEIGHT = 2 # Header占有行数
9
- FOOTER_HEIGHT = 1 # Footer占有行数
10
- HEADER_FOOTER_MARGIN = 4 # Header + Footer分のマージン
9
+ HEADER_HEIGHT = 1 # Header占有行数
10
+ FOOTER_HEIGHT = 1 # Footer占有行数(ブックマーク一覧 + ステータス情報)
11
+ HEADER_FOOTER_MARGIN = 3 # Header + Footer分のマージン
11
12
 
12
13
  # Panel layout ratios
13
14
  LEFT_PANEL_RATIO = 0.5 # 左パネルの幅比率
@@ -17,8 +18,6 @@ module Rufio
17
18
  DEFAULT_SCREEN_WIDTH = 80 # デフォルト画面幅
18
19
  DEFAULT_SCREEN_HEIGHT = 24 # デフォルト画面高さ
19
20
  HEADER_PADDING = 2 # ヘッダーのパディング
20
- BASE_INFO_RESERVED_WIDTH = 20 # ベースディレクトリ表示の予約幅
21
- BASE_INFO_MIN_WIDTH = 10 # ベースディレクトリ表示の最小幅
22
21
  FILTER_TEXT_RESERVED = 15 # フィルタテキスト表示の予約幅
23
22
 
24
23
  # File display constants
@@ -31,7 +30,7 @@ module Rufio
31
30
  GIGABYTE = MEGABYTE * 1024
32
31
 
33
32
  # Line offsets
34
- CONTENT_START_LINE = 3 # コンテンツ開始行(ヘッダー2行スキップ)
33
+ CONTENT_START_LINE = 2 # コンテンツ開始行(ヘッダー1行スキップ)
35
34
 
36
35
  def initialize
37
36
  console = IO.console
@@ -116,6 +115,9 @@ module Rufio
116
115
  end
117
116
 
118
117
  def draw_screen
118
+ # 処理時間測定開始
119
+ start_time = Time.now
120
+
119
121
  # move cursor to top of screen (don't clear)
120
122
  print "\e[H"
121
123
 
@@ -125,9 +127,8 @@ module Rufio
125
127
  return
126
128
  end
127
129
 
128
- # header (2 lines)
130
+ # header (1 line)
129
131
  draw_header
130
- draw_base_directory_info
131
132
 
132
133
  # main content (left: directory list, right: preview)
133
134
  entries = get_display_entries
@@ -144,8 +145,9 @@ module Rufio
144
145
  draw_directory_list(entries, left_width, content_height)
145
146
  draw_file_preview(selected_entry, right_width, content_height, left_width)
146
147
 
147
- # footer
148
- draw_footer
148
+ # footer (統合されたステータス情報を含む)
149
+ render_time = Time.now - start_time
150
+ draw_footer(render_time)
149
151
 
150
152
  # コマンドモードがアクティブな場合はコマンド入力ウィンドウを表示
151
153
  if @command_mode_active
@@ -163,6 +165,11 @@ module Rufio
163
165
  current_path = @directory_listing.current_path
164
166
  header = "📁 rufio - #{current_path}"
165
167
 
168
+ # Add help mode indicator if in help mode
169
+ if @keybind_handler.help_mode?
170
+ header += " [Help Mode - Press ESC to exit]"
171
+ end
172
+
166
173
  # Add filter indicator if in filter mode
167
174
  if @keybind_handler.filter_active?
168
175
  filter_text = " [Filter: #{@keybind_handler.filter_query}]"
@@ -171,7 +178,12 @@ module Rufio
171
178
 
172
179
  # abbreviate if path is too long
173
180
  if header.length > @screen_width - HEADER_PADDING
174
- if @keybind_handler.filter_active?
181
+ if @keybind_handler.help_mode?
182
+ # prioritize showing help mode indicator
183
+ help_text = " [Help Mode - Press ESC to exit]"
184
+ base_length = @screen_width - help_text.length - FILTER_TEXT_RESERVED
185
+ header = "📁 rufio - ...#{current_path[-base_length..-1]}#{help_text}"
186
+ elsif @keybind_handler.filter_active?
175
187
  # prioritize showing filter when active
176
188
  filter_text = " [Filter: #{@keybind_handler.filter_query}]"
177
189
  base_length = @screen_width - filter_text.length - FILTER_TEXT_RESERVED
@@ -184,42 +196,6 @@ module Rufio
184
196
  puts "\e[7m#{header.ljust(@screen_width)}\e[0m" # reverse display
185
197
  end
186
198
 
187
- def draw_base_directory_info
188
- # 強制的に表示 - デバッグ用に安全チェックを緩和
189
- if @keybind_handler && @keybind_handler.instance_variable_get(:@base_directory)
190
- base_dir = @keybind_handler.instance_variable_get(:@base_directory)
191
- selected_count = @keybind_handler.selected_items.length
192
- base_info = "📋 Base Directory: #{base_dir}"
193
-
194
- # 選択されたアイテム数を表示
195
- if selected_count > 0
196
- base_info += " | Selected: #{selected_count} item(s)"
197
- end
198
- else
199
- # keybind_handlerがない場合、またはbase_directoryが設定されていない場合
200
- base_info = "📋 Base Directory: #{Dir.pwd}"
201
- end
202
-
203
- # 長すぎる場合は省略
204
- if base_info.length > @screen_width - HEADER_PADDING
205
- if base_info.include?(" | Selected:")
206
- selected_part = base_info.split(" | Selected:").last
207
- available_length = @screen_width - BASE_INFO_RESERVED_WIDTH - " | Selected:#{selected_part}".length
208
- else
209
- available_length = @screen_width - BASE_INFO_RESERVED_WIDTH
210
- end
211
-
212
- if available_length > BASE_INFO_MIN_WIDTH
213
- # パスの最後の部分を表示
214
- dir_part = base_info.split(": ").last.split(" | ").first
215
- short_base_dir = "...#{dir_part[-available_length..-1]}"
216
- base_info = base_info.gsub(dir_part, short_base_dir)
217
- end
218
- end
219
-
220
- # 2行目に確実に表示
221
- print "\e[2;1H\e[44m\e[37m#{base_info.ljust(@screen_width)}\e[0m"
222
- end
223
199
 
224
200
 
225
201
  def draw_directory_list(entries, width, height)
@@ -348,12 +324,19 @@ module Rufio
348
324
  if selected_entry && i == 0
349
325
  # プレビューヘッダー
350
326
  header = " #{selected_entry[:name]} "
327
+ # プレビューフォーカス中は表示を追加
328
+ if @keybind_handler&.preview_focused?
329
+ header += "[PREVIEW MODE]"
330
+ end
351
331
  content_to_print = header
352
332
  elsif selected_entry && selected_entry[:type] == 'file' && i >= 2
353
333
  # ファイルプレビュー(折り返し対応)
354
334
  preview_content = get_preview_content(selected_entry)
355
- wrapped_lines = wrap_preview_lines(preview_content, safe_width - 1) # スペース分を除く
356
- display_line_index = i - 2
335
+ wrapped_lines = TextUtils.wrap_preview_lines(preview_content, safe_width - 1) # スペース分を除く
336
+
337
+ # スクロールオフセットを適用
338
+ scroll_offset = @keybind_handler&.preview_scroll_offset || 0
339
+ display_line_index = i - 2 + scroll_offset
357
340
 
358
341
  if display_line_index < wrapped_lines.length
359
342
  line = wrapped_lines[display_line_index] || ''
@@ -370,16 +353,16 @@ module Rufio
370
353
  if safe_width <= 0
371
354
  # 表示スペースがない場合は何も出力しない
372
355
  next
373
- elsif display_width(content_to_print) > safe_width
356
+ elsif TextUtils.display_width(content_to_print) > safe_width
374
357
  # 表示幅ベースで切り詰める
375
- content_to_print = truncate_to_width(content_to_print, safe_width)
358
+ content_to_print = TextUtils.truncate_to_width(content_to_print, safe_width)
376
359
  end
377
360
 
378
361
  # 出力(パディングなし、はみ出し防止のため)
379
362
  print content_to_print
380
363
 
381
364
  # 残りのスペースを埋める(ただし安全な範囲内のみ)
382
- remaining_space = safe_width - display_width(content_to_print)
365
+ remaining_space = safe_width - TextUtils.display_width(content_to_print)
383
366
  print ' ' * remaining_space if remaining_space > 0
384
367
  end
385
368
  end
@@ -402,98 +385,6 @@ module Rufio
402
385
  ["(#{ConfigLoader.message('file.preview_error')})"]
403
386
  end
404
387
 
405
- def wrap_preview_lines(lines, max_width)
406
- return [] if lines.empty? || max_width <= 0
407
-
408
- wrapped_lines = []
409
-
410
- lines.each do |line|
411
- if display_width(line) <= max_width
412
- # 短い行はそのまま追加
413
- wrapped_lines << line
414
- else
415
- # 長い行は折り返し
416
- remaining_line = line
417
- while display_width(remaining_line) > max_width
418
- # 単語境界で折り返すことを試みる
419
- break_point = find_break_point(remaining_line, max_width)
420
- wrapped_lines << remaining_line[0...break_point]
421
- remaining_line = remaining_line[break_point..-1]
422
- end
423
- # 残りの部分を追加
424
- wrapped_lines << remaining_line if remaining_line.length > 0
425
- end
426
- end
427
-
428
- wrapped_lines
429
- end
430
-
431
- def display_width(string)
432
- # 文字列の表示幅を計算する
433
- # 日本語文字(全角)は幅2、ASCII文字(半角)は幅1として計算
434
- width = 0
435
- string.each_char do |char|
436
- # 全角文字の判定
437
- width += if char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/)
438
- 2
439
- else
440
- 1
441
- end
442
- end
443
- width
444
- end
445
-
446
- def truncate_to_width(string, max_width)
447
- # 表示幅を指定して文字列を切り詰める
448
- return string if display_width(string) <= max_width
449
-
450
- current_width = 0
451
- result = ''
452
-
453
- string.each_char do |char|
454
- char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
455
-
456
- if current_width + char_width > max_width
457
- # "..."を追加できるかチェック
458
- result += '...' if max_width >= 3 && current_width <= max_width - 3
459
- break
460
- end
461
-
462
- result += char
463
- current_width += char_width
464
- end
465
-
466
- result
467
- end
468
-
469
- def find_break_point(line, max_width)
470
- # 最大幅以内で適切な折り返し位置を見つける
471
- return line.length if display_width(line) <= max_width
472
-
473
- # 文字ごとに幅を計算しながら適切な位置を探す
474
- current_width = 0
475
- best_break_point = 0
476
- space_break_point = nil
477
- punct_break_point = nil
478
-
479
- line.each_char.with_index do |char, index|
480
- char_width = char.ord > 127 || char.match?(/[あ-んア-ン一-龯]/) ? 2 : 1
481
-
482
- break if current_width + char_width > max_width
483
-
484
- current_width += char_width
485
- best_break_point = index + 1
486
-
487
- # スペースで区切れる位置を記録
488
- space_break_point = index + 1 if char == ' ' && current_width > max_width * 0.5
489
-
490
- # 日本語の句読点で区切れる位置を記録
491
- punct_break_point = index + 1 if char.match?(/[、。,.!?]/) && current_width > max_width * 0.5
492
- end
493
-
494
- # 最適な折り返し位置を選択
495
- space_break_point || punct_break_point || best_break_point
496
- end
497
388
 
498
389
  def get_display_entries
499
390
  if @keybind_handler.filter_active?
@@ -506,9 +397,9 @@ module Rufio
506
397
  end
507
398
  end
508
399
 
509
- def draw_footer
510
- # 最下行から1行上に表示してスクロールを避ける
511
- footer_line = @screen_height - FOOTER_HEIGHT
400
+ def draw_footer(render_time = nil)
401
+ # フッタは最下行に表示
402
+ footer_line = @screen_height - FOOTER_HEIGHT + 1
512
403
  print "\e[#{footer_line};1H"
513
404
 
514
405
  if @keybind_handler.filter_active?
@@ -517,14 +408,50 @@ module Rufio
517
408
  else
518
409
  help_text = "Filtered view active - Space to edit filter, ESC to clear filter"
519
410
  end
411
+ # フィルタモードでは通常のフッタを表示
412
+ footer_content = help_text.ljust(@screen_width)[0...@screen_width]
413
+ print "\e[7m#{footer_content}\e[0m"
520
414
  else
521
- help_text = ConfigLoader.message('help.full')
522
- help_text = ConfigLoader.message('help.short') if help_text.length > @screen_width
523
- end
415
+ # 通常モードではブックマーク一覧、ステータス情報、?:helpを1行に表示
416
+ require_relative 'bookmark'
417
+ bookmark = Bookmark.new
418
+ bookmarks = bookmark.list
419
+
420
+ # 起動ディレクトリを取得
421
+ start_dir = @directory_listing&.start_directory
422
+ start_dir_name = if start_dir
423
+ File.basename(start_dir)
424
+ else
425
+ "start"
426
+ end
427
+
428
+ # ブックマーク一覧を作成(0.起動dir を先頭に追加)
429
+ bookmark_parts = ["0.#{start_dir_name}"]
430
+ unless bookmarks.empty?
431
+ bookmark_parts.concat(bookmarks.take(9).map.with_index(1) { |bm, idx| "#{idx}.#{bm[:name]}" })
432
+ end
433
+ bookmark_text = bookmark_parts.join(" ")
524
434
 
525
- # 文字列を確実に画面幅に合わせる
526
- footer_content = help_text.ljust(@screen_width)[0...@screen_width]
527
- print "\e[7m#{footer_content}\e[0m"
435
+ # ステータス情報を作成
436
+ time_info = render_time ? "#{(render_time * 1000).round(1)}ms" : "-ms"
437
+
438
+ # 右側の情報: 処理時間 | ?:help
439
+ right_info = "#{time_info} | ?:help"
440
+
441
+ # ブックマーク一覧を利用可能な幅に収める
442
+ available_width = @screen_width - right_info.length - 3
443
+ if bookmark_text.length > available_width && available_width > 3
444
+ bookmark_text = bookmark_text[0...available_width - 3] + "..."
445
+ elsif available_width <= 3
446
+ bookmark_text = ""
447
+ end
448
+
449
+ # フッタ全体を構築
450
+ padding = @screen_width - bookmark_text.length - right_info.length
451
+ footer_content = "#{bookmark_text}#{' ' * padding}#{right_info}"
452
+ footer_content = footer_content.ljust(@screen_width)[0...@screen_width]
453
+ print "\e[7m#{footer_content}\e[0m"
454
+ end
528
455
  end
529
456
 
530
457
  def handle_input
@@ -786,11 +713,12 @@ module Rufio
786
713
  is_project_selected = (bookmark[:name] == selected_name)
787
714
  selection_mark = is_project_selected ? "✓ " : " "
788
715
 
789
- # ブックマーク名を表示
716
+ # ブックマーク名を表示(番号付き)
717
+ number = index + 1 # 1-based index
790
718
  name = bookmark[:name]
791
- max_name_length = width - 4 # selection_mark分を除く
719
+ max_name_length = width - 8 # selection_mark(2) + number(1-2) + ". "(2) + padding
792
720
  display_name = name.length > max_name_length ? name[0...max_name_length - 3] + '...' : name
793
- line_content = "#{selection_mark}#{display_name}".ljust(width)
721
+ line_content = "#{selection_mark}#{number}. #{display_name}".ljust(width)
794
722
 
795
723
  if index == current_index
796
724
  # カーソル位置は選択色でハイライト
@@ -994,6 +922,69 @@ module Rufio
994
922
  draw_screen
995
923
  end
996
924
 
925
+ # ヘルプダイアログを表示
926
+ def show_help_dialog
927
+ content_lines = [
928
+ '',
929
+ "rufio v#{VERSION}",
930
+ '',
931
+ 'Key Bindings:',
932
+ '',
933
+ 'j/k - Move up/down',
934
+ 'h/l - Navigate back/enter',
935
+ 'g/G - Go to top/bottom',
936
+ 'o - Open file',
937
+ 'f - Filter files',
938
+ 's - Search with fzf',
939
+ 'F - Content search (rga)',
940
+ 'a/A - Create file/directory',
941
+ 'm/c/x - Move/Copy/Delete',
942
+ 'b - Add bookmark',
943
+ 'z - Zoxide navigation',
944
+ '0 - Go to start directory',
945
+ '1-9 - Go to bookmark',
946
+ 'p - Project mode',
947
+ ': - Command mode',
948
+ 'q - Quit',
949
+ ''
950
+ ]
951
+
952
+ # お知らせ情報を追加
953
+ require_relative 'info_notice'
954
+ info_notice = InfoNotice.new
955
+ all_notices = Dir.glob(File.join(info_notice.info_dir, '*.txt'))
956
+
957
+ if !all_notices.empty?
958
+ content_lines << 'Recent Updates:'
959
+ content_lines << ''
960
+ all_notices.take(3).each do |file|
961
+ title = info_notice.extract_title(file)
962
+ content_lines << " • #{title}"
963
+ end
964
+ content_lines << ''
965
+ end
966
+
967
+ content_lines << 'Press any key to continue...'
968
+
969
+ width = 60
970
+ height = [content_lines.length + 4, @screen_height - 4].min
971
+ x, y = @dialog_renderer.calculate_center(width, height)
972
+
973
+ @dialog_renderer.draw_floating_window(x, y, width, height, 'rufio - Help', content_lines, {
974
+ border_color: "\e[36m", # Cyan
975
+ title_color: "\e[1;36m", # Bold cyan
976
+ content_color: "\e[37m" # White
977
+ })
978
+
979
+ require 'io/console'
980
+ IO.console.getch
981
+ @dialog_renderer.clear_area(x, y, width, height)
982
+
983
+ # 画面を再描画
984
+ refresh_display
985
+ draw_screen
986
+ end
987
+
997
988
  # プロジェクトモードでコマンドを実行
998
989
  def activate_project_command_mode(project_mode, project_command, project_log)
999
990
  return unless project_mode.selected_path
@@ -104,5 +104,54 @@ module Rufio
104
104
 
105
105
  space_break_point || punct_break_point || best_break_point
106
106
  end
107
+
108
+ # Wrap preview lines to fit within max_width
109
+ # @param lines [Array<String>] Lines to wrap
110
+ # @param max_width [Integer] Maximum width for each line
111
+ # @return [Array<String>] Wrapped lines
112
+ def wrap_preview_lines(lines, max_width)
113
+ return lines if max_width <= 0
114
+
115
+ wrapped = []
116
+ lines.each do |line|
117
+ # Remove trailing whitespace
118
+ line = line.rstrip
119
+
120
+ # If line is empty, keep it
121
+ if line.empty?
122
+ wrapped << ''
123
+ next
124
+ end
125
+
126
+ # If line fits within max_width, keep it as is
127
+ if display_width(line) <= max_width
128
+ wrapped << line
129
+ next
130
+ end
131
+
132
+ # Split long lines
133
+ current_line = []
134
+ current_width = 0
135
+
136
+ line.each_char do |char|
137
+ char_width = display_width(char)
138
+
139
+ if current_width + char_width > max_width
140
+ # Start a new line
141
+ wrapped << current_line.join
142
+ current_line = [char]
143
+ current_width = char_width
144
+ else
145
+ current_line << char
146
+ current_width += char_width
147
+ end
148
+ end
149
+
150
+ # Add remaining characters
151
+ wrapped << current_line.join unless current_line.empty?
152
+ end
153
+
154
+ wrapped
155
+ end
107
156
  end
108
157
  end
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '0.20.0'
4
+ VERSION = '0.30.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rufio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
@@ -119,6 +119,8 @@ files:
119
119
  - CHANGELOG.md
120
120
  - CHANGELOG_v0.10.0.md
121
121
  - CHANGELOG_v0.20.0.md
122
+ - CHANGELOG_v0.21.0.md
123
+ - CHANGELOG_v0.30.0.md
122
124
  - CHANGELOG_v0.4.0.md
123
125
  - CHANGELOG_v0.5.0.md
124
126
  - CHANGELOG_v0.6.0.md
@@ -132,7 +134,9 @@ files:
132
134
  - config_example.rb
133
135
  - docs/PLUGIN_GUIDE.md
134
136
  - docs/plugin_example.rb
135
- - info/welcome.txt
137
+ - info/help.md
138
+ - info/keybindings.md
139
+ - info/welcome.md
136
140
  - lib/rufio.rb
137
141
  - lib/rufio/application.rb
138
142
  - lib/rufio/bookmark.rb
data/info/welcome.txt DELETED
@@ -1,21 +0,0 @@
1
- # Welcome to rufio!
2
-
3
- Thank you for using rufio - a terminal-based file manager.
4
-
5
- Key Features:
6
- - Vim-like keybindings (j/k/h/l for navigation)
7
- - Real-time filtering (f key)
8
- - Advanced search with fzf and rga
9
- - Bookmark support (b key)
10
- - Plugin system for extensibility
11
-
12
- Quick Start:
13
- j/k - Move up/down
14
- h/l - Parent dir / Enter dir
15
- f - Filter files
16
- b - Bookmarks
17
- : - Command mode
18
- q - Quit
19
-
20
- For more information, visit:
21
- https://github.com/masisz/rufio