rufio 1.0.4 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 431baab2112b7382ee6b9aeb19a17f7cbc0133adc8d76e349ac614827908234b
4
- data.tar.gz: ffe6479dc44a44869dbd56a67af086cfda84164cacb6b3b8334e0d691f24a706
3
+ metadata.gz: c22a65655be19fc13d4e565ea8f926186fc58c7acbff0e16daed2a0efd3362b1
4
+ data.tar.gz: 654b40fe283c94abc1c438b77a2a8eeabc5e5a6321acd01e056d6ccaa5af874b
5
5
  SHA512:
6
- metadata.gz: 92038826f2b9ba53ba7c2e329c835ba92f99e9678151805faadbdc23f32f7fe6b4bcb4cb8394855ef1ddb01a96ce1abc4ee12ffbf0f1b71672b2148ba7692daa
7
- data.tar.gz: e7dfbeba635500c59b6fbd2c3b17efdd4b17c24c6bcac9af7d95bb6ebc7ef38b59ce55be6206dd42ceb49b2531728a21cd8c1876a44189371be6f3621ac7c2fc
6
+ metadata.gz: afb532e1ae55826d8ffeb2607b31717bbae6a2c322493f9a5d81f308bad8c995c714e6638376fbba5ed3878689193917b29814e0ed657d02905a9d8ba8a873c5
7
+ data.tar.gz: 97e60e1b5e180eccaf10fb2c5e501f52ec51dde50fc32f0fa5c5f11c2cbf89b367f970c693f5e2d533efea928d1ccd3a8a9e9aed72c4a1830e0975fb9033a980
data/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ 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
+ ## [1.1.0] - 2026-10-25
9
+
10
+ ### Added
11
+ - G mode shows a yellow animated `Searching...` indicator even while no matches arrive, and switches to `Search complete` when scanning finishes
12
+ - Configure G mode file name and content search exclusions with `GLOBAL_SEARCH_EXCLUDED_EXTENSIONS` in `config.rb`, supporting case-insensitive and compound extensions
13
+ - G mode content search: Use Tab to switch between file names and literal text search, with matching lines and line numbers, Japanese text support, and background cancellation when editing the query
14
+ - C mode: Open `~/.config/rufio/` with the same navigation, preview, and file operations as Files mode; create the directory if needed
15
+ - G mode: Search file names recursively across all bookmarked directories, with background scanning, incremental filtering, and Enter to locate a result in Files mode
16
+
17
+ ### Changed
18
+ - Move the bottom-of-list shortcut from `G` to `}` to make room for global search
19
+
8
20
  ## [1.0.4] - 2026-10-25
9
21
 
10
22
  ### Changed
data/README.md CHANGED
@@ -84,7 +84,7 @@ rufio /path/to # Launch in specified directory
84
84
  |-----|----------|
85
85
  | `j/k` | Move up/down |
86
86
  | `h/l` | Parent/child directory |
87
- | `g/G` | Top/bottom |
87
+ | `g/}` | Top/bottom |
88
88
  | `Enter` | Enter directory/open file |
89
89
  | `q` | Quit |
90
90
 
@@ -123,11 +123,43 @@ rufio /path/to # Launch in specified directory
123
123
  | Key | Function |
124
124
  |-----|----------|
125
125
  | `:` | Command mode |
126
+ | `G` | Search file names or contents across all bookmarked directories |
127
+ | `C` | Open ~/.config/rufio/ in Config mode (same controls as Files) |
126
128
  | `J` | Job mode |
127
129
  | `L` | View execution logs |
128
130
  | `?` | Help |
129
131
  | `Shift+Tab` | Switch mode (reverse) |
130
132
 
133
+ ### Global Search (G mode)
134
+
135
+ Press `G` to search recursively across all bookmarked directories. `Tab`
136
+ switches between **File names** and **Content**. Type a literal string to
137
+ search (case insensitive); content results include the file, line number,
138
+ and matching text. Text is read as UTF-8; files containing NUL bytes in the
139
+ first 8 KB are skipped as binary. Use Up/Down or Ctrl+N/P to select a
140
+ result, and press Enter to jump to that file in Files mode. `Ctrl+U` clears
141
+ the query; `Esc` cancels and returns to your previous view. Scanning runs in
142
+ the background and skips inaccessible directories and directory symlinks.
143
+ Add bookmarks with `b` before searching. G mode needs no external search tools.
144
+ While scanning, a yellow `Searching...` indicator animates even before matches
145
+ arrive. It turns green and displays `Search complete` when the scan finishes.
146
+
147
+ To exclude extensions from both file name and content searches, add this to
148
+ `~/.config/rufio/config.rb` and restart Rufio:
149
+
150
+ ```ruby
151
+ GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.png .jpg .zip .tar.gz .log].freeze
152
+ ```
153
+
154
+ Extensions are case insensitive and the leading dot is optional. The default
155
+ is `[]` (no extension exclusions). Directory names are unaffected.
156
+
157
+ ### Config Mode (C)
158
+
159
+ Press `C` to open `~/.config/rufio/`, creating it if needed. Navigation, previews,
160
+ and file operations work as in Files mode. Browse settings and scripts directly;
161
+ leaving the config directory returns the mode display to Files.
162
+
131
163
  ## Command Mode
132
164
 
133
165
  Press `:` to enter command mode and execute various commands.
data/README_ja.md CHANGED
@@ -84,7 +84,7 @@ rufio /path/to # 指定したディレクトリで起動
84
84
  |------|------|
85
85
  | `j/k` | 上下移動 |
86
86
  | `h/l` | 親/子ディレクトリ |
87
- | `g/G` | 先頭/末尾 |
87
+ | `g/}` | 先頭/末尾 |
88
88
  | `Enter` | ディレクトリに入る/ファイルを開く |
89
89
  | `q` | 終了 |
90
90
 
@@ -126,8 +126,40 @@ rufio /path/to # 指定したディレクトリで起動
126
126
  | `J` | ジョブモード |
127
127
  | `L` | 実行ログ表示 |
128
128
  | `?` | ヘルプ |
129
+ | `G` | 全ブックマーク配下のファイル名・ファイル内容を横断検索 |
130
+ | `C` | ~/.config/rufio/を開く(Configモード、操作はFilesと共通) |
129
131
  | `Shift+Tab` | モード切り替え(逆順) |
130
132
 
133
+ ### Gモード(横断検索)
134
+
135
+ `G` を押すと、ブックマークした全ディレクトリの配下を再帰的に検索します。
136
+ Tabで **File names(ファイル名)** と **Content(ファイル内容)** を切り替えます。
137
+ 大文字・小文字を区別しない文字列の部分一致で検索し、内容検索ではファイル名・行番号・一致した行を表示します。
138
+ テキストはUTF-8として読み込み、先頭8 KBにNULバイトがあるファイルはバイナリとしてスキップします。
139
+ 上下矢印または `Ctrl+N/P` で選択、
140
+ Enterで選んだファイルの場所へ移動します。`Ctrl+U` で検索語をクリアし、
141
+ Escで元の表示に戻ります。検索はバックグラウンドで進みます。
142
+ 検索対象は `b` で登録したブックマークです。外部検索ツールは不要です。
143
+ アクセスできないディレクトリはスキップし、ディレクトリのシンボリックリンクは辿りません。
144
+ 検索中は黄色の `Searching...` と動くスピナーを表示します。
145
+ 結果がまだ見つかっていない間も表示が動き、完了すると緑の `Search complete` に切り替わります。
146
+
147
+ ファイル名検索・内容検索から特定の拡張子を除外するには、
148
+ `~/.config/rufio/config.rb` に以下を追加してRufioを再起動します。
149
+
150
+ ```ruby
151
+ GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.png .jpg .zip .tar.gz .log].freeze
152
+ ```
153
+
154
+ 大文字・小文字は区別せず、先頭のドットは省略できます。デフォルトは `[]`(拡張子の除外なし)です。
155
+ ディレクトリ名には適用しません。
156
+
157
+ ### Configモード(C)
158
+
159
+ `C` で `~/.config/rufio/` を開きます。ディレクトリがなければ作成します。
160
+ 移動・プレビュー・ファイル操作はFilesモードと共通で、設定やスクリプトを直接編集できます。
161
+ 設定ディレクトリの外へ移動すると、モード表示がFilesに戻ります。
162
+
131
163
  ## コマンドモード
132
164
 
133
165
  `:` でコマンドモードを起動し、様々なコマンドを実行できます。
data/examples/config.rb CHANGED
@@ -40,6 +40,14 @@ KEYBINDS = {
40
40
  open_file: %w[o SPACE]
41
41
  }.freeze
42
42
 
43
+ # ========================================
44
+ # G Mode Search Exclusions
45
+ # ========================================
46
+ # Exclude these extensions from both file name and content searches.
47
+ # Case insensitive; a leading dot is optional. Compound suffixes work too.
48
+ # Example: %w[.png .jpg .zip .tar.gz .log]
49
+ GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = [].freeze
50
+
43
51
  # ========================================
44
52
  # Application Associations
45
53
  # ========================================
data/lib/rufio/config.rb CHANGED
@@ -56,7 +56,7 @@ module Rufio
56
56
  'ui.operation_prompt' => 'Operation: ',
57
57
 
58
58
  # Help text
59
- 'help.full' => 'j/k:move h:back l:enter o:open g/G:top/bottom r:refresh f:filter s:search F:content a/A:create m/p/x:ops b:bookmark z:zoxide 1-9:goto q:quit',
59
+ 'help.full' => 'j/k:move h:back l:enter o:open g/}:top/bottom G:global-search r:refresh f:filter s:search F:content a/A:create m/p/x:ops b:bookmark z:zoxide 1-9:goto q:quit',
60
60
  'help.short' => 'j/k:move h:back l:enter o:open f:filter s:search b:bookmark z:zoxide 1-9:goto q:quit',
61
61
 
62
62
  # Health check messages
@@ -126,7 +126,7 @@ module Rufio
126
126
  'ui.operation_prompt' => 'Operation: ',
127
127
 
128
128
  # Help text
129
- 'help.full' => 'j/k:move h:back l:enter o:open g/G:top/end r:refresh f:filter s:search F:content a/A:create m/c/x:ops b:bookmark z:zoxide 1-9:jump q:quit',
129
+ 'help.full' => 'j/k:move h:back l:enter o:open g/}:top/end G:global-search r:refresh f:filter s:search F:content a/A:create m/c/x:ops b:bookmark z:zoxide 1-9:jump q:quit',
130
130
  'help.short' => 'j/k:move h:back l:enter o:open f:filter s:search b:bookmark z:zoxide 1-9:jump q:quit',
131
131
 
132
132
  # Health check messages
@@ -47,7 +47,7 @@ module Rufio
47
47
  navigate_parent: 'h',
48
48
  navigate_enter: 'l',
49
49
  top: 'g',
50
- bottom: 'G',
50
+ bottom: '}',
51
51
  refresh: 'R',
52
52
  # ファイル操作
53
53
  open_file: 'o',
@@ -72,6 +72,8 @@ module Rufio
72
72
  start_dir: '0',
73
73
  # モード・ツール
74
74
  job_mode: 'J',
75
+ global_search: 'G',
76
+ config_mode: 'C',
75
77
  help: '?',
76
78
  log_viewer: 'L',
77
79
  command_mode: ':',
@@ -100,6 +102,18 @@ module Rufio
100
102
  default_keybinds.merge(user_keybinds)
101
103
  end
102
104
 
105
+ def global_search_excluded_extensions
106
+ values = load_config[:global_search_excluded_extensions]
107
+ return [] unless values.is_a?(Array)
108
+
109
+ values.select { |value| value.is_a?(String) }.map do |value|
110
+ extension = value.strip.downcase
111
+ next if extension.empty? || extension == '.'
112
+
113
+ extension.start_with?('.') ? extension : ".#{extension}"
114
+ end.compact.uniq
115
+ end
116
+
103
117
  def language
104
118
  load_config[:language] || Config.current_language
105
119
  end
@@ -286,7 +300,8 @@ module Rufio
286
300
  config = {
287
301
  applications: safe_const_get(:APPLICATIONS, default_config[:applications]),
288
302
  colors: safe_const_get(:COLORS, default_config[:colors]),
289
- keybinds: safe_const_get(:KEYBINDS, {})
303
+ keybinds: safe_const_get(:KEYBINDS, {}),
304
+ global_search_excluded_extensions: safe_const_get(:GLOBAL_SEARCH_EXCLUDED_EXTENSIONS, [])
290
305
  }
291
306
 
292
307
  if Object.const_defined?(:LANGUAGE)
@@ -335,6 +350,7 @@ module Rufio
335
350
  selected: { hsl: [50, 90, 70] },
336
351
  preview: { hsl: [180, 60, 65] }
337
352
  },
353
+ global_search_excluded_extensions: [],
338
354
  keybinds: {}, # ユーザーの上書きのみ格納(デフォルトはdefault_keybindsを参照)
339
355
  ui_options: {} # ユーザーの上書きのみ格納(デフォルトはdefault_ui_optionsを参照)
340
356
  }
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+ require_relative 'text_utils'
5
+ require_relative 'config_loader'
6
+
7
+ module Rufio
8
+ # ブックマーク配下のファイル名・テキスト内容をバックグラウンドで横断検索する。
9
+ class GlobalSearch
10
+ SPINNER_FRAMES = ['|', '/', '-', '\\'].freeze
11
+ SPINNER_INTERVAL = 0.12
12
+
13
+ attr_reader :query, :results, :errors, :selected_index, :search_type, :skipped_files
14
+
15
+ def initialize
16
+ @active = false
17
+ @scanning = false
18
+ @worker = nil
19
+ end
20
+
21
+ def active?
22
+ @active
23
+ end
24
+
25
+ def scanning?
26
+ @scanning
27
+ end
28
+
29
+ def open(bookmarks)
30
+ close
31
+ @active = true
32
+ @query = ''
33
+ @search_type = :filename
34
+ @excluded_extensions = ConfigLoader.global_search_excluded_extensions
35
+ @bookmark_count = bookmarks.length
36
+ @roots = bookmarks.map { |bookmark| [bookmark[:name].to_s, bookmark[:path].to_s] }
37
+ start_scan
38
+ true
39
+ end
40
+
41
+ def close
42
+ @active = false
43
+ @scanning = false
44
+ @worker&.kill
45
+ @worker&.join
46
+ @worker = nil
47
+ end
48
+
49
+ # 1フレーム当たりの処理量を制限し、検索中も入力を受け付ける。
50
+ def poll
51
+ return false unless active?
52
+
53
+ changed = false
54
+ 256.times do
55
+ event = begin
56
+ @events.pop(true)
57
+ rescue ThreadError
58
+ break
59
+ end
60
+ changed = true
61
+ type, value = event
62
+ case type
63
+ when :entry
64
+ @entries << value
65
+ @results << value if @search_type == :content || matches_query?(value)
66
+ when :error then @errors << value
67
+ when :skipped then @skipped_files += 1
68
+ when :done then @scanning = false
69
+ end
70
+ end
71
+ if scanning?
72
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
73
+ if now - @last_spinner_update >= SPINNER_INTERVAL
74
+ @spinner_index = (@spinner_index + 1) % SPINNER_FRAMES.length
75
+ @last_spinner_update = now
76
+ changed = true
77
+ end
78
+ end
79
+ changed
80
+ end
81
+
82
+ def handle_key(key)
83
+ case key
84
+ when "\e" then close
85
+ when "\t"
86
+ @search_type = @search_type == :filename ? :content : :filename
87
+ start_scan
88
+ when "\r", "\n" then return @results[@selected_index]
89
+ when :down then @selected_index = [@selected_index + 1, [@results.length - 1, 0].max].min
90
+ when :up then @selected_index = [@selected_index - 1, 0].max
91
+ when "\x0e" then handle_key(:down) # Ctrl+N
92
+ when "\x10" then handle_key(:up) # Ctrl+P
93
+ when "\x15" then update_query('') # Ctrl+U
94
+ when "\u007f", "\b" then update_query(@query[0...-1])
95
+ else
96
+ update_query(@query + key) if key.is_a?(String) && !key.empty? && key.each_codepoint.all? { |c| c >= 32 && c != 127 }
97
+ end
98
+ nil
99
+ end
100
+
101
+ def draw(screen)
102
+ screen.clear
103
+ width = screen.width
104
+ type = @search_type == :content ? 'Content' : 'File names'
105
+ write_line(screen, 0, "G mode - #{type} | Tab: Switch name/content search", width, "\e[1;36m")
106
+ write_line(screen, 1, "Search: #{@query}_", width)
107
+ state = if scanning?
108
+ "[#{SPINNER_FRAMES[@spinner_index]}] Searching..."
109
+ elsif @search_type == :content && @query.empty?
110
+ 'Ready'
111
+ else
112
+ 'Search complete'
113
+ end
114
+ color = scanning? ? "\e[1;33m" : "\e[32m"
115
+ write_line(screen, 2, "#{state} | #{@bookmark_count} bookmarks | #{@results.length} matches | #{@errors.length} errors | #{@skipped_files} skipped", width, color)
116
+ row_height = @search_type == :content ? 2 : 1
117
+ capacity = [screen.height - 6, 0].max / row_height
118
+ first = [@selected_index - capacity + 1, 0].max
119
+ @results.slice(first, capacity).to_a.each_with_index do |entry, index|
120
+ selected = first + index == @selected_index
121
+ text = "#{selected ? '>' : ' '} [#{entry[:bookmark]}] #{entry[:relative_path]}"
122
+ text += ":#{entry[:line_number]}" if entry[:line_number]
123
+ y = index * row_height + 3
124
+ write_line(screen, y, text, width, selected ? "\e[1;36m" : nil)
125
+ write_line(screen, y + 1, " #{entry[:match_text]}", width) if entry[:line_number]
126
+ end
127
+ if @results.empty?
128
+ message = if @bookmark_count.zero?
129
+ 'No bookmarks. Add directories with b in Files mode.'
130
+ elsif scanning?
131
+ 'Searching bookmarked directories...'
132
+ elsif @search_type == :content && @query.empty?
133
+ 'Type a string to search inside text files.'
134
+ elsif !@errors.empty? && @entries.empty?
135
+ 'No readable files in bookmarked directories.'
136
+ else
137
+ 'No matching files. Edit your search to try again.'
138
+ end
139
+ write_line(screen, 3, message, width)
140
+ end
141
+ write_line(screen, screen.height - 3, @results[@selected_index]&.[](:path) || @errors.first.to_s, width, "\e[90m")
142
+ write_line(screen, screen.height - 2, 'ESC: Back | Enter: Go to file | Up/Down: Select | Type: Search', width)
143
+ end
144
+
145
+ private
146
+
147
+ def start_scan
148
+ @worker&.kill
149
+ @worker&.join
150
+ @worker = nil
151
+ @entries = []
152
+ @results = []
153
+ @errors = []
154
+ @skipped_files = 0
155
+ @selected_index = 0
156
+ @events = SizedQueue.new(256)
157
+ @spinner_index = 0
158
+ @last_spinner_update = Process.clock_gettime(Process::CLOCK_MONOTONIC)
159
+ @scanning = !(@search_type == :content && @query.empty?)
160
+ return unless @scanning
161
+
162
+ roots, events, type, query = @roots, @events, @search_type, @query.downcase
163
+ @worker = Thread.new do
164
+ begin
165
+ scan_roots(roots, events, type, query)
166
+ rescue StandardError => e
167
+ events << [:error, e.message]
168
+ end
169
+ events << [:done, nil]
170
+ end
171
+ end
172
+
173
+ def update_query(query)
174
+ @query = query
175
+ @selected_index = 0
176
+ @search_type == :content ? start_scan : filter_results
177
+ end
178
+
179
+ def filter_results
180
+ query = @query.downcase
181
+ @results = @entries.select { |entry| entry[:relative_path].scrub.downcase.include?(query) }
182
+ @selected_index = [[@selected_index, @results.length - 1].min, 0].max
183
+ end
184
+
185
+ def matches_query?(entry)
186
+ entry[:relative_path].scrub.downcase.include?(@query.downcase)
187
+ end
188
+
189
+ def scan_roots(roots, events, type, query)
190
+ visited_dirs = {}
191
+ roots.each do |name, root|
192
+ begin
193
+ root = File.expand_path(root)
194
+ raise Errno::ENOTDIR, root unless File.directory?(root)
195
+
196
+ pending = [root]
197
+ until pending.empty?
198
+ directory = pending.pop
199
+ canonical = File.realpath(directory)
200
+ next if visited_dirs[canonical]
201
+
202
+ visited_dirs[canonical] = true
203
+ begin
204
+ Dir.each_child(directory) do |child|
205
+ path = File.join(directory, child)
206
+ # ディレクトリシンボリックリンクは追わず、循環・範囲外の走査を防ぐ。
207
+ if File.directory?(path)
208
+ pending << path unless File.symlink?(path)
209
+ elsif File.file?(path)
210
+ if child.scrub.downcase.end_with?(*@excluded_extensions)
211
+ events << [:skipped, nil]
212
+ next
213
+ end
214
+
215
+ entry = { path: path, bookmark: name, relative_path: path.delete_prefix(root + File::SEPARATOR) }
216
+ if type == :content
217
+ scan_content(entry, query, events)
218
+ else
219
+ events << [:entry, entry]
220
+ end
221
+ end
222
+ end
223
+ rescue SystemCallError, IOError => e
224
+ events << [:error, "#{directory}: #{e.message}"]
225
+ end
226
+ end
227
+ rescue SystemCallError, IOError => e
228
+ events << [:error, "#{name}: #{e.message}"]
229
+ end
230
+ end
231
+ end
232
+
233
+ def scan_content(entry, query, events)
234
+ File.open(entry[:path], 'rb') do |file|
235
+ sample = file.read(8192).to_s
236
+ if sample.include?("\0")
237
+ events << [:skipped, nil]
238
+ return
239
+ end
240
+
241
+ file.rewind
242
+ file.each_line.with_index(1) do |raw_line, number|
243
+ line = raw_line.force_encoding(Encoding::UTF_8).scrub
244
+ match_index = line.downcase.index(query)
245
+ next unless match_index
246
+
247
+ snippet_start = [match_index - 30, 0].max
248
+ snippet = line[snippet_start, 200].strip
249
+ events << [:entry, entry.merge(line_number: number, match_text: snippet)]
250
+ end
251
+ end
252
+ rescue SystemCallError, IOError => e
253
+ events << [:error, "#{entry[:path]}: #{e.message}"]
254
+ end
255
+
256
+ def write_line(screen, y, text, width, color = nil)
257
+ safe_text = text.scrub.gsub(/[[:cntrl:]]/, ' ')
258
+ screen.put_string(0, y, TextUtils.truncate_to_width(safe_text, width), fg: color)
259
+ end
260
+ end
261
+ end
@@ -68,6 +68,7 @@ module Rufio
68
68
  "#{key(:filter)}: Filter | Enter: Apply | ESC: Clear",
69
69
  "#{key(:fzf_search)}: Filename search (fzf)",
70
70
  "#{key(:rga_search)}: Content search (rga)",
71
+ "#{key(:global_search)}: Search file names or contents across all bookmarks",
71
72
  "#{key(:add_bookmark)}/#{key(:bookmark_menu)}: Add / manage bookmarks",
72
73
  'Tab / Shift+Tab: Next / previous bookmark',
73
74
  "1-9: Jump to bookmark | #{key(:start_dir)}: Start directory",
@@ -77,6 +78,24 @@ module Rufio
77
78
  when 'Modes'
78
79
  [
79
80
  'Files', 'Browse directories, select files and view previews.',
81
+ '', "[#{key(:config_mode)}] Config",
82
+ 'Open ~/.config/rufio/ to edit settings and scripts.',
83
+ 'The config directory is created if it does not exist.',
84
+ 'Navigation, previews and file operations work as in Files.',
85
+ "#{key(:navigate_parent)}: Parent directory; leaving config returns to Files.",
86
+ '', "[#{key(:global_search)}] Global search",
87
+ 'Search file names or text inside every bookmarked directory.',
88
+ 'Tab: Switch File names / Content (literal, case insensitive)',
89
+ 'Content results show file, line number and matching text.',
90
+ 'Yellow Searching... + spinner: Search is running.',
91
+ 'Green Search complete: All bookmarked directories scanned.',
92
+ 'Exclude extensions in ~/.config/rufio/config.rb:',
93
+ 'GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.zip .log]',
94
+ 'Applies to names and contents. Restart Rufio after editing.',
95
+ 'Extensions ignore case; leading dots are optional; [] excludes none.',
96
+ 'Type to filter | Up/Down or Ctrl+N/P: Select',
97
+ 'Enter: Go to the selected file in Files mode',
98
+ 'ESC: Return to the previous mode | Ctrl+U: Clear query',
80
99
  '', "[#{key(:log_viewer)}] Logs",
81
100
  'Browse command logs. Enter opens a preview.',
82
101
  'ESC: Leave preview; ESC again: Return to Files.',
@@ -25,6 +25,40 @@ module Rufio
25
25
  @nav_controller.preview_focused?
26
26
  end
27
27
 
28
+ def global_search_mode?
29
+ @terminal_ui&.global_search_active? || false
30
+ end
31
+
32
+ def start_global_search
33
+ @terminal_ui&.start_global_search(@bookmark_manager.list)
34
+ end
35
+
36
+ def config_directory
37
+ Config::CONFIG_DIR
38
+ end
39
+
40
+ def config_mode?
41
+ return false if @in_log_viewer_mode || @in_help_mode || @job_mode.active?
42
+
43
+ path = @directory_listing&.current_path
44
+ root = File.expand_path(config_directory)
45
+ path && (path == root || path.start_with?(root + File::SEPARATOR))
46
+ end
47
+
48
+ def enter_config_mode
49
+ return false unless @directory_listing
50
+
51
+ FileUtils.mkdir_p(config_directory)
52
+ exit_log_viewer_mode if @in_log_viewer_mode
53
+ exit_help_mode if @in_help_mode
54
+ exit_job_mode if @job_mode.active?
55
+ @nav_controller.unfocus_preview_pane
56
+ navigate_to_directory(config_directory)
57
+ rescue SystemCallError => e
58
+ @notification_manager.add("Cannot open config directory: #{e.message}", :error, duration: 0)
59
+ false
60
+ end
61
+
28
62
  def preview_scroll_offset
29
63
  @nav_controller.preview_scroll_offset
30
64
  end
@@ -134,6 +168,14 @@ module Rufio
134
168
  return show_help_dialog
135
169
  end
136
170
 
171
+ if @key_map[key] == :global_search && !@filter_manager.filter_mode
172
+ return start_global_search
173
+ end
174
+
175
+ if @key_map[key] == :config_mode && !@filter_manager.filter_mode
176
+ return enter_config_mode
177
+ end
178
+
137
179
  # ジョブモード中の特別処理
138
180
  if @job_mode.active?
139
181
  return handle_job_mode_key(key)
@@ -4,10 +4,12 @@ module Rufio
4
4
  # タブモード管理クラス
5
5
  # 上部メニューの2段目に表示するモードタブを管理する
6
6
  class TabModeManager
7
- MODES = %i[files logs jobs help].freeze
7
+ MODES = %i[files config global logs jobs help].freeze
8
8
 
9
9
  MODE_LABELS = {
10
10
  files: 'Files',
11
+ config: 'Config',
12
+ global: 'Search',
11
13
  logs: 'Logs',
12
14
  jobs: 'Jobs',
13
15
  help: 'Help'
@@ -15,6 +17,8 @@ module Rufio
15
17
 
16
18
  MODE_KEYS = {
17
19
  files: nil,
20
+ config: 'C',
21
+ global: 'G',
18
22
  logs: 'L',
19
23
  jobs: 'J',
20
24
  help: '?'
@@ -3,6 +3,7 @@
3
3
  require 'io/console'
4
4
  require_relative 'text_utils'
5
5
  require_relative 'help_menu'
6
+ require_relative 'global_search'
6
7
 
7
8
  module Rufio
8
9
  class TerminalUI
@@ -53,6 +54,7 @@ module Rufio
53
54
  @input_thread = nil
54
55
  @command_mode_active = false
55
56
  @help_menu = HelpMenu.new
57
+ @global_search = GlobalSearch.new
56
58
  @command_input = ""
57
59
  @command_mode = CommandMode.new
58
60
  @dialog_renderer = DialogRenderer.new
@@ -304,6 +306,7 @@ module Rufio
304
306
  end
305
307
 
306
308
  def cleanup_terminal
309
+ @global_search.close
307
310
  # マウスレポートを先に無効化して新しいイベントの流入を止める
308
311
  if windows?
309
312
  print "\e[?1000l\e[?1006l"
@@ -475,6 +478,8 @@ module Rufio
475
478
  needs_redraw = true
476
479
  end
477
480
 
481
+ needs_redraw = true if @global_search.poll
482
+
478
483
  # 非同期シンタックスハイライト完了チェック(UIRenderer側のフラグを参照)
479
484
  if @ui_renderer.highlight_updated?
480
485
  @ui_renderer.reset_highlight_updated
@@ -529,6 +534,12 @@ module Rufio
529
534
 
530
535
  # UIRenderer に全描画処理を委譲
531
536
  def draw_screen_to_buffer(screen, notification_message = nil, fps = nil)
537
+ if global_search_active?
538
+ @global_search.draw(screen)
539
+ @ui_renderer.draw_mode_tabs_to_buffer(screen, @screen_height - 1)
540
+ return
541
+ end
542
+
532
543
  @ui_renderer.draw_screen_to_buffer(
533
544
  screen, notification_message, fps,
534
545
  in_job_mode: @in_job_mode,
@@ -598,14 +609,14 @@ module Rufio
598
609
  return true
599
610
  end
600
611
  input = case third_char
601
- when 'A' then 'k' # Up arrow
602
- when 'B' then 'j' # Down arrow
603
- when 'C' then 'l' # Right arrow
604
- when 'D' then 'h' # Left arrow
612
+ when 'A' then global_search_active? && !help_dialog_active? ? :up : 'k' # Up arrow
613
+ when 'B' then global_search_active? && !help_dialog_active? ? :down : 'j' # Down arrow
614
+ when 'C' then global_search_active? && !help_dialog_active? ? :right : 'l' # Right arrow
615
+ when 'D' then global_search_active? && !help_dialog_active? ? :left : 'h' # Left arrow
605
616
  when 'Z'
606
617
  if help_dialog_active?
607
618
  handle_help_input("\e[Z")
608
- else
619
+ elsif !global_search_active?
609
620
  handle_shift_tab
610
621
  end
611
622
  return true # Shift+Tab
@@ -621,15 +632,16 @@ module Rufio
621
632
  end
622
633
 
623
634
  return handle_help_input(input) if help_dialog_active?
635
+ return handle_global_search_input(input) if global_search_active?
624
636
 
625
637
  # TabキーはFilesモードの時のみブックマーク循環移動(ジョブモード中は無効)
626
- if input == "\t" && @tab_mode_manager.current_mode == :files && !@in_job_mode
638
+ if input == "\t" && (@tab_mode_manager.current_mode == :files || @keybind_handler.config_mode?) && !@in_job_mode
627
639
  handle_tab_key
628
640
  return true
629
641
  end
630
642
 
631
643
  # 数字キー(1-9): Filesモード かつ フィルターモード外でブックマークジャンプ+ハイライト
632
- if input&.match?(/^[1-9]$/) && @tab_mode_manager.current_mode == :files && !@keybind_handler.filter_active?
644
+ if input&.match?(/^[1-9]$/) && (@tab_mode_manager.current_mode == :files || @keybind_handler.config_mode?) && !@keybind_handler.filter_active?
633
645
  handle_bookmark_key(input.to_i)
634
646
  return true
635
647
  end
@@ -682,7 +694,7 @@ module Rufio
682
694
 
683
695
  # Shift+Tab: Filesモードでは前のブックマークへ循環移動、それ以外はモード逆順切り替え
684
696
  def handle_shift_tab
685
- if @tab_mode_manager.current_mode == :files
697
+ if @tab_mode_manager.current_mode == :files || @keybind_handler.config_mode?
686
698
  prev_idx = @keybind_handler.goto_prev_bookmark
687
699
  if prev_idx
688
700
  @ui_renderer.set_highlighted_bookmark(prev_idx + 1)
@@ -710,6 +722,12 @@ module Rufio
710
722
  return
711
723
  end
712
724
 
725
+ if global_search_active?
726
+ handle_global_search_input(:up) if btn == 64
727
+ handle_global_search_input(:down) if btn == 65
728
+ return
729
+ end
730
+
713
731
  case btn
714
732
  when 0 # 左クリック(押下のみ処理)
715
733
  handle_mouse_left_click(col, row) if press
@@ -792,6 +810,10 @@ module Rufio
792
810
  # モード変更を適用
793
811
  def apply_mode_change(mode)
794
812
  case mode
813
+ when :config
814
+ @keybind_handler.enter_config_mode
815
+ when :global
816
+ @keybind_handler.start_global_search
795
817
  when :files
796
818
  # ヘルプモードまたはログビューワモードから戻る
797
819
  if @keybind_handler.help_mode?
@@ -1204,6 +1226,46 @@ module Rufio
1204
1226
  # Screen と Renderer のアクセサ(他のクラスから利用可能に)
1205
1227
  attr_reader :screen, :renderer
1206
1228
 
1229
+ def start_global_search(bookmarks)
1230
+ @global_search.open(bookmarks)
1231
+ end
1232
+
1233
+ def global_search_active?
1234
+ @global_search.active?
1235
+ end
1236
+
1237
+ def handle_global_search_input(input)
1238
+ if input == ConfigLoader.keybinds[:help]
1239
+ show_help_dialog
1240
+ return true
1241
+ end
1242
+ result = @global_search.handle_key(input)
1243
+ if result.is_a?(Hash) && File.file?(result[:path])
1244
+ @keybind_handler.exit_log_viewer_mode if @keybind_handler.log_viewer_mode?
1245
+ @keybind_handler.exit_help_mode if @keybind_handler.help_mode?
1246
+ @keybind_handler.exit_job_mode if @keybind_handler.in_job_mode?
1247
+ if @keybind_handler.send(:navigate_to_directory, File.dirname(result[:path]))
1248
+ index = @keybind_handler.get_active_entries.index { |entry| entry[:path] == result[:path] }
1249
+ @keybind_handler.select_index(index) if index
1250
+ @keybind_handler.unfocus_preview_pane
1251
+ @global_search.close
1252
+ end
1253
+ end
1254
+ unless global_search_active?
1255
+ mode = if @keybind_handler.in_job_mode?
1256
+ :jobs
1257
+ elsif @keybind_handler.log_viewer_mode?
1258
+ :logs
1259
+ elsif @keybind_handler.config_mode?
1260
+ :config
1261
+ else
1262
+ :files
1263
+ end
1264
+ @tab_mode_manager.switch_to(mode)
1265
+ end
1266
+ true
1267
+ end
1268
+
1207
1269
  # ヘルプは非同期オーバーレイとして表示し、元のモードを保持する。
1208
1270
  def show_help_dialog
1209
1271
  @help_menu.open
@@ -782,12 +782,16 @@ module Rufio
782
782
  def sync_tab_mode_with_keybind_handler
783
783
  return unless @keybind_handler
784
784
 
785
- current_mode = if @keybind_handler.in_job_mode?
785
+ current_mode = if @keybind_handler.global_search_mode?
786
+ :global
787
+ elsif @keybind_handler.in_job_mode?
786
788
  :jobs
787
789
  elsif @keybind_handler.help_mode?
788
790
  :help
789
791
  elsif @keybind_handler.log_viewer_mode?
790
792
  :logs
793
+ elsif @keybind_handler.config_mode?
794
+ :config
791
795
  else
792
796
  :files
793
797
  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 = '1.0.4'
4
+ VERSION = '1.1.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: 1.0.4
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
@@ -175,6 +175,7 @@ files:
175
175
  - lib/rufio/file_operations.rb
176
176
  - lib/rufio/file_preview.rb
177
177
  - lib/rufio/filter_manager.rb
178
+ - lib/rufio/global_search.rb
178
179
  - lib/rufio/health_checker.rb
179
180
  - lib/rufio/help_menu.rb
180
181
  - lib/rufio/info_notice.rb