rufio 1.4.1 → 1.5.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 +7 -0
- data/docs/guide.md +5 -5
- data/docs/guide_ja.md +5 -5
- data/info/help.md +1 -1
- data/lib/rufio/config_loader.rb +2 -2
- data/lib/rufio/global_search.rb +44 -5
- data/lib/rufio/help_menu.rb +2 -1
- 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 +32 -10
- data/lib/rufio/native_scanner_zig.rb +13 -0
- data/lib/rufio/version.rb +1 -1
- data/lib_zig/rufio_native/src/cli.zig +24 -10
- data/lib_zig/rufio_native/src/main.zig +16 -0
- data/lib_zig/rufio_native/src/search.zig +52 -5
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e68729d1cfbfe899d2bdb0c2626ab46958561319aa6a33f8224eb5aaae4e0ad7
|
|
4
|
+
data.tar.gz: 93bfc8f597d0c16339a168b1711abde8bc5251fb7cd8209b199c3d71f0e7068d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5631c3ef32be2d26bd9759d32a3c7680274b49291e14054bce8184821445ce9f3209a1c99ceeffcfcde0149d458215c2d3328ce363cff8c85f6b470b5ade7f5
|
|
7
|
+
data.tar.gz: 60dfadd02062abd388eb458083685f7b36fb98b0193b5a95a94bccdb6f7a8b962e59f46cbccef12e8962ad0e35688ef522d1dbf6a89bd44c6049717d93f2e602
|
data/CHANGELOG.md
CHANGED
|
@@ -7,8 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.5.0] - 2026-11-14
|
|
11
|
+
|
|
10
12
|
### Added
|
|
11
13
|
- G mode supports `Ctrl+J` / `Ctrl+K` to select the next / previous result; Enter (CR) opens the selected result
|
|
14
|
+
- `GLOBAL_SEARCH_EXCLUDED_EXTENSIONS` accepts names starting with `/` (for example `/.git`, `/node_modules`, `/.DS_Store`) to skip a directory and everything under it, or files with exactly that name. Names ignore case, and extension entries such as `.png` keep working as before
|
|
15
|
+
- The bundled Zig library adds a file listing mode (`core_files_create`) that walks directories in parallel without opening files, rebuilt for all eight bundled platforms. The `rufio_search` benchmark CLI gains `--files` for comparison with `fd`
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- G mode file name search walks bookmarked directories with the parallel Zig engine instead of a single Ruby thread. `RUFIO_NATIVE_SEARCH=0` or an older library without the file listing function falls back to the Ruby walk
|
|
12
19
|
|
|
13
20
|
## [1.4.1] - 2026-11-08
|
|
14
21
|
|
data/docs/guide.md
CHANGED
|
@@ -140,15 +140,15 @@ Content search also reads documents, without Python, Microsoft Office or convers
|
|
|
140
140
|
|
|
141
141
|
Extracted text is cached in memory (up to 64 documents, about 16 MiB) and re-extracted when a file changes. Scanned images need OCR, which is not included. Legacy `.xls` / `.ppt` and slide notes are not supported.
|
|
142
142
|
|
|
143
|
-
### Excluding Extensions
|
|
143
|
+
### Excluding Extensions and Directories
|
|
144
144
|
|
|
145
145
|
Add this to `~/.config/rufio/config.rb` and restart rufio. It applies to both file names and contents:
|
|
146
146
|
|
|
147
147
|
```ruby
|
|
148
|
-
GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.png .jpg .zip .tar.gz .log].freeze
|
|
148
|
+
GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.png .jpg .zip .tar.gz .log /.git /node_modules /.DS_Store].freeze
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
Extensions ignore case, the leading dot is optional, and the default `[]` excludes nothing.
|
|
151
|
+
Extensions ignore case, the leading dot is optional, and the default `[]` excludes nothing. An entry starting with `/` is a name: a directory with that name is skipped with everything under it, and a file with exactly that name is skipped.
|
|
152
152
|
|
|
153
153
|
## Bookmarks and History
|
|
154
154
|
|
|
@@ -323,8 +323,8 @@ APPLICATIONS = {
|
|
|
323
323
|
:default => 'open'
|
|
324
324
|
}.freeze
|
|
325
325
|
|
|
326
|
-
# Extensions excluded from G mode search
|
|
327
|
-
GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.log].freeze
|
|
326
|
+
# Extensions (and /names of directories or files) excluded from G mode search
|
|
327
|
+
GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.log /node_modules].freeze
|
|
328
328
|
```
|
|
329
329
|
|
|
330
330
|
Action names for `KEYBINDS`:
|
data/docs/guide_ja.md
CHANGED
|
@@ -140,15 +140,15 @@ Python・Microsoft Office・変換ツールなしで文書の中も検索でき
|
|
|
140
140
|
|
|
141
141
|
抽出した文字列はメモリにキャッシュし(最大 64 文書・約 16 MiB)、ファイルが変わると再抽出します。画像だけの PDF には OCR が必要で、これは含みません。旧形式の `.xls` / `.ppt` とスライドノートは対象外です。
|
|
142
142
|
|
|
143
|
-
###
|
|
143
|
+
### 拡張子・ディレクトリの除外
|
|
144
144
|
|
|
145
145
|
`~/.config/rufio/config.rb` に次を追加して rufio を再起動します。ファイル名・内容の両方に適用されます。
|
|
146
146
|
|
|
147
147
|
```ruby
|
|
148
|
-
GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.png .jpg .zip .tar.gz .log].freeze
|
|
148
|
+
GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.png .jpg .zip .tar.gz .log /.git /node_modules /.DS_Store].freeze
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
大文字・小文字は区別せず、先頭のドットは省略できます。デフォルトの `[]`
|
|
151
|
+
大文字・小文字は区別せず、先頭のドットは省略できます。デフォルトの `[]` は何も除外しません。`/` で始まる要素は名前の指定になり、その名前のディレクトリは配下ごと、名前が完全に一致するファイルは単体で除外します。
|
|
152
152
|
|
|
153
153
|
## ブックマークと履歴
|
|
154
154
|
|
|
@@ -323,8 +323,8 @@ APPLICATIONS = {
|
|
|
323
323
|
:default => 'open'
|
|
324
324
|
}.freeze
|
|
325
325
|
|
|
326
|
-
# G
|
|
327
|
-
GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.log].freeze
|
|
326
|
+
# G モードの検索から除外する拡張子(/ 始まりはディレクトリ・ファイル名)
|
|
327
|
+
GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.log /node_modules].freeze
|
|
328
328
|
```
|
|
329
329
|
|
|
330
330
|
`KEYBINDS` で使えるアクション名:
|
data/info/help.md
CHANGED
|
@@ -149,7 +149,7 @@ Search file names or text inside every bookmarked directory.
|
|
|
149
149
|
- Content search also reads `.pdf` (page), `.xlsx` (sheet/cell) and `.pptx` (slide)
|
|
150
150
|
- `Up/Down` or `Ctrl+J/K or Ctrl+N/P`: Select a result | `Enter`: Go to the file in Files mode
|
|
151
151
|
- `Ctrl+U`: Clear the query | `ESC`: Return to the previous mode
|
|
152
|
-
- Exclude extensions with `GLOBAL_SEARCH_EXCLUDED_EXTENSIONS` in `~/.config/rufio/config.rb`
|
|
152
|
+
- Exclude extensions (or `/name` directories and files) with `GLOBAL_SEARCH_EXCLUDED_EXTENSIONS` in `~/.config/rufio/config.rb`
|
|
153
153
|
|
|
154
154
|
### Config Mode (`C`)
|
|
155
155
|
|
data/lib/rufio/config_loader.rb
CHANGED
|
@@ -110,9 +110,9 @@ module Rufio
|
|
|
110
110
|
|
|
111
111
|
values.select { |value| value.is_a?(String) }.map do |value|
|
|
112
112
|
extension = value.strip.downcase
|
|
113
|
-
next if extension.empty? || extension == '.'
|
|
113
|
+
next if extension.empty? || extension == '.' || extension == '/'
|
|
114
114
|
|
|
115
|
-
extension.start_with?('.') ? extension : ".#{extension}"
|
|
115
|
+
extension.start_with?('.', '/') ? extension : ".#{extension}"
|
|
116
116
|
end.compact.uniq
|
|
117
117
|
end
|
|
118
118
|
|
data/lib/rufio/global_search.rb
CHANGED
|
@@ -83,7 +83,7 @@ module Rufio
|
|
|
83
83
|
when :entries
|
|
84
84
|
# Zig 実装の結果はまとめて届くので、1 フレーム 256 件の上限に縛られない
|
|
85
85
|
@entries.concat(value)
|
|
86
|
-
@results.concat(value)
|
|
86
|
+
@results.concat(@search_type == :content || @query.empty? ? value : value.select { |entry| matches_query?(entry) })
|
|
87
87
|
when :error then @errors << value
|
|
88
88
|
when :skipped then @skipped_files += value || 1
|
|
89
89
|
when :done then @scanning = false
|
|
@@ -190,11 +190,14 @@ module Rufio
|
|
|
190
190
|
return unless @scanning
|
|
191
191
|
|
|
192
192
|
roots, events, type, query = @roots, @events, @search_type, @query.downcase
|
|
193
|
-
|
|
193
|
+
native = self.class.native_search?
|
|
194
|
+
native_cache = native && type == :content ? (@native_cache ||= NativeContentSearch::Cache.new) : nil
|
|
194
195
|
@worker = Thread.new do
|
|
195
196
|
begin
|
|
196
|
-
if
|
|
197
|
+
if native && type == :content && NativeContentSearch.supports_query?(query)
|
|
197
198
|
scan_roots_natively(roots, events, query, native_cache)
|
|
199
|
+
elsif native && type == :filename && NativeContentSearch.files_available?
|
|
200
|
+
scan_files_natively(roots, events)
|
|
198
201
|
else
|
|
199
202
|
scan_roots(roots, events, type, query)
|
|
200
203
|
end
|
|
@@ -223,6 +226,8 @@ module Rufio
|
|
|
223
226
|
|
|
224
227
|
def scan_roots(roots, events, type, query)
|
|
225
228
|
visited_dirs = {}
|
|
229
|
+
excluded_names = @excluded_extensions.select { |e| e.start_with?('/') }.map { |e| e.delete_prefix('/') }
|
|
230
|
+
excluded_suffixes = @excluded_extensions.reject { |e| e.start_with?('/') }
|
|
226
231
|
roots.each do |name, root|
|
|
227
232
|
begin
|
|
228
233
|
root = File.expand_path(root)
|
|
@@ -238,11 +243,12 @@ module Rufio
|
|
|
238
243
|
begin
|
|
239
244
|
Dir.each_child(directory) do |child|
|
|
240
245
|
path = File.join(directory, child)
|
|
246
|
+
folded_child = child.scrub.downcase
|
|
241
247
|
# ディレクトリシンボリックリンクは追わず、循環・範囲外の走査を防ぐ。
|
|
242
248
|
if File.directory?(path)
|
|
243
|
-
pending << path unless File.symlink?(path)
|
|
249
|
+
pending << path unless File.symlink?(path) || excluded_names.include?(folded_child)
|
|
244
250
|
elsif File.file?(path)
|
|
245
|
-
if
|
|
251
|
+
if excluded_names.include?(folded_child) || (!excluded_suffixes.empty? && folded_child.end_with?(*excluded_suffixes))
|
|
246
252
|
events << [:skipped, nil]
|
|
247
253
|
next
|
|
248
254
|
end
|
|
@@ -265,6 +271,39 @@ module Rufio
|
|
|
265
271
|
end
|
|
266
272
|
end
|
|
267
273
|
|
|
274
|
+
# Zig 実装でブックマーク配下のファイル一覧を並列走査する
|
|
275
|
+
def scan_files_natively(roots, events)
|
|
276
|
+
valid_roots = []
|
|
277
|
+
roots.each do |name, root|
|
|
278
|
+
root = File.expand_path(root)
|
|
279
|
+
raise Errno::ENOTDIR, root unless File.directory?(root)
|
|
280
|
+
|
|
281
|
+
valid_roots << [name, root]
|
|
282
|
+
rescue SystemCallError, IOError => e
|
|
283
|
+
events << [:error, "#{name}: #{e.message}"]
|
|
284
|
+
end
|
|
285
|
+
return if valid_roots.empty?
|
|
286
|
+
|
|
287
|
+
engine = NativeContentSearch.new(roots: valid_roots.map(&:last), query: nil,
|
|
288
|
+
excluded_extensions: @excluded_extensions)
|
|
289
|
+
engine.each_batch do |batch|
|
|
290
|
+
entries = []
|
|
291
|
+
batch.each do |kind, *args|
|
|
292
|
+
case kind
|
|
293
|
+
when :file, :match
|
|
294
|
+
name, root = valid_roots[args[0]]
|
|
295
|
+
path = args[1]
|
|
296
|
+
entries << { path: path, bookmark: name, relative_path: path.delete_prefix(root + File::SEPARATOR) }
|
|
297
|
+
when :error then events << [:error, args[1]]
|
|
298
|
+
when :skipped then events << [:skipped, args[0]]
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
events << [:entries, entries] unless entries.empty?
|
|
302
|
+
end
|
|
303
|
+
ensure
|
|
304
|
+
engine&.close
|
|
305
|
+
end
|
|
306
|
+
|
|
268
307
|
# Zig 実装で走査・検索し、PDF などのドキュメントだけ Ruby 側で抽出する
|
|
269
308
|
def scan_roots_natively(roots, events, query, cache)
|
|
270
309
|
valid_roots = []
|
data/lib/rufio/help_menu.rb
CHANGED
|
@@ -100,9 +100,10 @@ module Rufio
|
|
|
100
100
|
'Yellow Searching... + spinner: Search is running.',
|
|
101
101
|
'Green Search complete: All bookmarked directories scanned.',
|
|
102
102
|
'Exclude extensions in ~/.config/rufio/config.rb:',
|
|
103
|
-
'GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.zip .log]',
|
|
103
|
+
'GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.zip .log /node_modules]',
|
|
104
104
|
'Applies to names and contents. Restart Rufio after editing.',
|
|
105
105
|
'Extensions ignore case; leading dots are optional; [] excludes none.',
|
|
106
|
+
'/name skips a directory with that name, or a file with exactly that name.',
|
|
106
107
|
'Type to filter | Up/Down or Ctrl+J/K or Ctrl+N/P: Select',
|
|
107
108
|
'Enter: Go to the selected file in Files mode',
|
|
108
109
|
'ESC: Return to the previous mode | Ctrl+U: Clear query',
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -50,6 +50,10 @@ module Rufio
|
|
|
50
50
|
NativeScannerZigFFI.search_available?
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def self.files_available?
|
|
54
|
+
NativeScannerZigFFI.files_available?
|
|
55
|
+
end
|
|
56
|
+
|
|
53
57
|
# Zig 側は ASCII のみ大文字小文字を畳み込むため、大文字小文字を持つ非 ASCII 文字は Ruby 実装に任せる
|
|
54
58
|
def self.supports_query?(query)
|
|
55
59
|
return false if query.empty? || query.bytesize > MAX_QUERY_BYTES
|
|
@@ -64,20 +68,33 @@ module Rufio
|
|
|
64
68
|
line[[match_index - 30, 0].max, 200].to_s.strip
|
|
65
69
|
end
|
|
66
70
|
|
|
67
|
-
def initialize(roots:, query
|
|
68
|
-
|
|
71
|
+
def initialize(roots:, query: nil, excluded_extensions: [], document_extensions: [], cache: nil, threads: 0)
|
|
72
|
+
@files_only = query.nil?
|
|
73
|
+
if @files_only
|
|
74
|
+
raise StandardError, 'Zig file search is not available' unless self.class.files_available?
|
|
75
|
+
else
|
|
76
|
+
raise StandardError, 'Zig content search is not available' unless self.class.available?
|
|
77
|
+
end
|
|
69
78
|
|
|
70
79
|
@cache = cache
|
|
71
80
|
@path_encoding = Encoding.find('filesystem')
|
|
72
81
|
roots_buf = pack(roots)
|
|
73
|
-
query_buf = query.b
|
|
74
82
|
excluded_buf = pack(excluded_extensions)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
@handle = if @files_only
|
|
84
|
+
NativeScannerZigFFI.core_files_create(
|
|
85
|
+
roots_buf, roots_buf.bytesize,
|
|
86
|
+
excluded_buf, excluded_buf.bytesize,
|
|
87
|
+
threads
|
|
88
|
+
)
|
|
89
|
+
else
|
|
90
|
+
query_buf = query.b
|
|
91
|
+
documents_buf = pack(document_extensions)
|
|
92
|
+
NativeScannerZigFFI.core_search_create(
|
|
93
|
+
roots_buf, roots_buf.bytesize, query_buf, query_buf.bytesize,
|
|
94
|
+
excluded_buf, excluded_buf.bytesize, documents_buf, documents_buf.bytesize,
|
|
95
|
+
threads, cache ? cache.handle : 0
|
|
96
|
+
)
|
|
97
|
+
end
|
|
81
98
|
raise StandardError, 'Failed to start native search' if @handle.zero?
|
|
82
99
|
|
|
83
100
|
@length = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
@@ -143,7 +160,12 @@ module Rufio
|
|
|
143
160
|
position += extra_length
|
|
144
161
|
|
|
145
162
|
case KINDS[kind]
|
|
146
|
-
when :match
|
|
163
|
+
when :match
|
|
164
|
+
if @files_only
|
|
165
|
+
yield [:file, root, path]
|
|
166
|
+
else
|
|
167
|
+
yield [:match, root, path, line, self.class.snippet(extra, offset)]
|
|
168
|
+
end
|
|
147
169
|
when :document then yield [:document, root, path]
|
|
148
170
|
when :error then yield [:error, root, "#{path}: #{extra.force_encoding(Encoding::UTF_8).scrub}"]
|
|
149
171
|
end
|
|
@@ -101,6 +101,12 @@ module Rufio
|
|
|
101
101
|
@search_available == true
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# ファイル一覧走査 API(古いライブラリには存在しない)
|
|
105
|
+
def files_available?
|
|
106
|
+
load! unless @loaded
|
|
107
|
+
@files_available == true
|
|
108
|
+
end
|
|
109
|
+
|
|
104
110
|
# ZIP データ処理 API(古いライブラリには存在しない)
|
|
105
111
|
def zip_available?
|
|
106
112
|
load! unless @loaded
|
|
@@ -125,9 +131,16 @@ module Rufio
|
|
|
125
131
|
extern 'uint64_t core_cache_bytes(uint64_t)'
|
|
126
132
|
extern 'void core_cache_destroy(uint64_t)'
|
|
127
133
|
@search_available = true
|
|
134
|
+
begin
|
|
135
|
+
extern 'uint64_t core_files_create(const char*, size_t, const char*, size_t, uint32_t)'
|
|
136
|
+
@files_available = true
|
|
137
|
+
rescue Fiddle::DLError
|
|
138
|
+
@files_available = false
|
|
139
|
+
end
|
|
128
140
|
rescue Fiddle::DLError => e
|
|
129
141
|
warn "Zig content search unavailable: #{e.message}" if ENV['RUFIO_DEBUG']
|
|
130
142
|
@search_available = false
|
|
143
|
+
@files_available = false
|
|
131
144
|
end
|
|
132
145
|
|
|
133
146
|
def load_zip_functions!
|
data/lib/rufio/version.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
//! ripgrep と同条件で比較するためのベンチマーク用 CLI。
|
|
2
|
-
//! 使い方:
|
|
1
|
+
//! ripgrep / fd と同条件で比較するためのベンチマーク用 CLI。
|
|
2
|
+
//! 使い方:
|
|
3
|
+
//! rufio_search [-j N] [-r N] QUERY DIR...
|
|
4
|
+
//! rufio_search --files [-j N] [DIR...]
|
|
3
5
|
//! -r N: キャッシュを使って同じ検索を N 回繰り返し、各回の所要時間を標準エラーに出す(G mode の再検索を再現)
|
|
4
|
-
//! 出力は rg -n と同じ `path:line:text`
|
|
6
|
+
//! 出力は rg -n と同じ `path:line:text` 形式(--files 時は `path` のみ)。
|
|
5
7
|
const std = @import("std");
|
|
6
8
|
const c = std.c;
|
|
7
9
|
const search = @import("search.zig");
|
|
@@ -17,6 +19,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
|
|
|
17
19
|
_ = args.skip();
|
|
18
20
|
var threads: usize = 0;
|
|
19
21
|
var repeat: usize = 0;
|
|
22
|
+
var files_only = false;
|
|
20
23
|
var query: ?[]const u8 = null;
|
|
21
24
|
var roots: std.ArrayList(u8) = .empty;
|
|
22
25
|
const alloc = std.heap.c_allocator;
|
|
@@ -27,18 +30,24 @@ pub fn main(init: std.process.Init.Minimal) !void {
|
|
|
27
30
|
threads = try std.fmt.parseInt(usize, args.next() orelse return error.MissingValue, 10);
|
|
28
31
|
} else if (std.mem.eql(u8, arg, "-r")) {
|
|
29
32
|
repeat = try std.fmt.parseInt(usize, args.next() orelse return error.MissingValue, 10);
|
|
30
|
-
} else if (
|
|
33
|
+
} else if (std.mem.eql(u8, arg, "--files")) {
|
|
34
|
+
files_only = true;
|
|
35
|
+
} else if (!files_only and query == null) {
|
|
31
36
|
query = arg;
|
|
32
37
|
} else {
|
|
33
38
|
if (roots.items.len > 0) try roots.append(alloc, 0);
|
|
34
39
|
try roots.appendSlice(alloc, std.mem.trimEnd(u8, arg, "/"));
|
|
35
40
|
}
|
|
36
41
|
}
|
|
42
|
+
if (roots.items.len == 0) try roots.append(alloc, '.');
|
|
43
|
+
if (files_only) {
|
|
44
|
+
_ = try runSearch(roots.items, null, threads, null, true);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
37
47
|
const q = query orelse {
|
|
38
|
-
writeAll("usage: rufio_search [-j N] [-r N] QUERY DIR...\n");
|
|
48
|
+
writeAll("usage: rufio_search [--files] [-j N] [-r N] [QUERY] DIR...\n");
|
|
39
49
|
return;
|
|
40
50
|
};
|
|
41
|
-
if (roots.items.len == 0) try roots.append(alloc, '.');
|
|
42
51
|
|
|
43
52
|
if (repeat > 0) {
|
|
44
53
|
const cache = try search.Cache.create(256 * 1024 * 1024);
|
|
@@ -56,9 +65,12 @@ pub fn main(init: std.process.Init.Minimal) !void {
|
|
|
56
65
|
_ = try runSearch(roots.items, q, threads, null, true);
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
fn runSearch(roots: []const u8, q: []const u8, threads: usize, cache: ?*search.Cache, print: bool) !usize {
|
|
68
|
+
fn runSearch(roots: []const u8, q: ?[]const u8, threads: usize, cache: ?*search.Cache, print: bool) !usize {
|
|
60
69
|
const alloc = std.heap.c_allocator;
|
|
61
|
-
const s =
|
|
70
|
+
const s = if (q) |query_str|
|
|
71
|
+
try search.Search.create(roots, query_str, "", "", threads, cache)
|
|
72
|
+
else
|
|
73
|
+
try search.Search.createFiles(roots, "/.git\x00/node_modules\x00/.ds_store", threads);
|
|
62
74
|
defer s.destroy();
|
|
63
75
|
|
|
64
76
|
var lines: usize = 0;
|
|
@@ -81,8 +93,10 @@ fn runSearch(roots: []const u8, q: []const u8, threads: usize, cache: ?*search.C
|
|
|
81
93
|
lines += 1;
|
|
82
94
|
if (!print) continue;
|
|
83
95
|
try out.appendSlice(alloc, a);
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
if (q != null) {
|
|
97
|
+
try out.appendSlice(alloc, try std.fmt.bufPrint(&line_buf, ":{d}:", .{line}));
|
|
98
|
+
try out.appendSlice(alloc, std.mem.trimEnd(u8, b, "\r\n"));
|
|
99
|
+
}
|
|
86
100
|
try out.append(alloc, '\n');
|
|
87
101
|
}
|
|
88
102
|
if (out.items.len >= 1 << 16) {
|
|
@@ -373,6 +373,22 @@ export fn core_search_create(
|
|
|
373
373
|
return @intFromPtr(s);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
/// ファイル一覧走査(fd 互換)を開始してハンドルを返す(失敗時は 0)。threads=0 で CPU 数
|
|
377
|
+
export fn core_files_create(
|
|
378
|
+
roots: [*]const u8,
|
|
379
|
+
roots_len: usize,
|
|
380
|
+
excluded: [*]const u8,
|
|
381
|
+
excluded_len: usize,
|
|
382
|
+
threads: u32,
|
|
383
|
+
) u64 {
|
|
384
|
+
const s = search.Search.createFiles(
|
|
385
|
+
roots[0..roots_len],
|
|
386
|
+
excluded[0..excluded_len],
|
|
387
|
+
threads,
|
|
388
|
+
) catch return 0;
|
|
389
|
+
return @intFromPtr(s);
|
|
390
|
+
}
|
|
391
|
+
|
|
376
392
|
export fn core_search_state(handle: u64) u8 {
|
|
377
393
|
if (handle == 0) return @intFromEnum(search.State.failed);
|
|
378
394
|
const s: *search.Search = @ptrFromInt(handle);
|
|
@@ -282,6 +282,7 @@ const Job = struct {
|
|
|
282
282
|
|
|
283
283
|
pub const Search = struct {
|
|
284
284
|
matcher: Matcher,
|
|
285
|
+
files_only: bool = false,
|
|
285
286
|
roots: StringList,
|
|
286
287
|
excluded: StringList,
|
|
287
288
|
documents: StringList,
|
|
@@ -313,10 +314,21 @@ pub const Search = struct {
|
|
|
313
314
|
|
|
314
315
|
pub fn create(roots: []const u8, query: []const u8, excluded: []const u8, documents: []const u8, threads: usize, cache: ?*Cache) !*Search {
|
|
315
316
|
if (query.len == 0 or query.len > MAX_NEEDLE or roots.len == 0) return error.InvalidArgument;
|
|
317
|
+
return createInternal(roots, query, excluded, documents, threads, cache, false);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// ファイル一覧列挙モード(fd 互換・ファイル内容は開かずに高速走査)
|
|
321
|
+
pub fn createFiles(roots: []const u8, excluded: []const u8, threads: usize) !*Search {
|
|
322
|
+
if (roots.len == 0) return error.InvalidArgument;
|
|
323
|
+
return createInternal(roots, "", excluded, "", threads, null, true);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn createInternal(roots: []const u8, query: []const u8, excluded: []const u8, documents: []const u8, threads: usize, cache: ?*Cache, files_only: bool) !*Search {
|
|
316
327
|
const self = try std.heap.c_allocator.create(Search);
|
|
317
328
|
errdefer std.heap.c_allocator.destroy(self);
|
|
318
329
|
self.* = .{
|
|
319
330
|
.matcher = Matcher.init(query),
|
|
331
|
+
.files_only = files_only,
|
|
320
332
|
.roots = try StringList.init(roots),
|
|
321
333
|
.excluded = try StringList.init(excluded),
|
|
322
334
|
.documents = try StringList.init(documents),
|
|
@@ -499,7 +511,9 @@ const Worker = struct {
|
|
|
499
511
|
key: Buf = .{},
|
|
500
512
|
|
|
501
513
|
fn init(self: *Worker) !void {
|
|
502
|
-
|
|
514
|
+
if (!self.s.files_only) {
|
|
515
|
+
try self.read.reserve(READ_BUFFER);
|
|
516
|
+
}
|
|
503
517
|
try self.scratch.init();
|
|
504
518
|
try self.out.reserve(LOCAL_FLUSH * 2);
|
|
505
519
|
}
|
|
@@ -520,7 +534,13 @@ const Worker = struct {
|
|
|
520
534
|
}
|
|
521
535
|
|
|
522
536
|
fn record(self: *Worker, kind: Kind, root: u32, line: u64, offset: u32, dir: []const u8, name: []const u8, extra: []const u8) void {
|
|
523
|
-
const
|
|
537
|
+
const clean_dir = if (name.len > 0 and std.mem.eql(u8, dir, "."))
|
|
538
|
+
""
|
|
539
|
+
else if (name.len > 0 and std.mem.startsWith(u8, dir, "./"))
|
|
540
|
+
dir[2..]
|
|
541
|
+
else
|
|
542
|
+
dir;
|
|
543
|
+
const path_len = if (name.len == 0) clean_dir.len else if (clean_dir.len == 0) name.len else clean_dir.len + 1 + name.len;
|
|
524
544
|
self.out.reserve(self.out.len + RECORD_HEADER + path_len + extra.len) catch return;
|
|
525
545
|
var hdr: [RECORD_HEADER]u8 = undefined;
|
|
526
546
|
hdr[0] = @intFromEnum(kind);
|
|
@@ -530,9 +550,13 @@ const Worker = struct {
|
|
|
530
550
|
std.mem.writeInt(u32, hdr[17..21], @intCast(path_len), .little);
|
|
531
551
|
std.mem.writeInt(u32, hdr[21..25], @intCast(extra.len), .little);
|
|
532
552
|
self.out.append(&hdr) catch unreachable;
|
|
533
|
-
|
|
553
|
+
if (clean_dir.len > 0) {
|
|
554
|
+
self.out.append(clean_dir) catch unreachable;
|
|
555
|
+
if (name.len > 0) {
|
|
556
|
+
self.out.append("/") catch unreachable;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
534
559
|
if (name.len > 0) {
|
|
535
|
-
self.out.append("/") catch unreachable;
|
|
536
560
|
self.out.append(name) catch unreachable;
|
|
537
561
|
}
|
|
538
562
|
self.out.append(extra) catch unreachable;
|
|
@@ -542,7 +566,17 @@ const Worker = struct {
|
|
|
542
566
|
self.record(.err, root, 0, 0, dir, name, os.lastErrorMessage());
|
|
543
567
|
}
|
|
544
568
|
|
|
569
|
+
fn isIgnoredDir(self: *Worker, name: []const u8) bool {
|
|
570
|
+
for (self.s.excluded.items) |ext| {
|
|
571
|
+
if (ext.len > 1 and ext[0] == '/' and name.len == ext.len - 1 and hasSuffixIgnoreCase(name, ext[1..])) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
|
|
545
578
|
fn pushSubdir(self: *Worker, job: *Job, name: []const u8) void {
|
|
579
|
+
if (self.isIgnoredDir(name)) return;
|
|
546
580
|
const sub = Job.create(job.root, job.path(), name) orelse return;
|
|
547
581
|
sub.next = self.subdirs;
|
|
548
582
|
if (self.subdirs == null) self.subdirs_tail = sub;
|
|
@@ -586,11 +620,24 @@ const Worker = struct {
|
|
|
586
620
|
|
|
587
621
|
fn handleFile(self: *Worker, dir: *os.Dir, job: *Job, name: [:0]const u8, size: ?u64, meta: ?FileMeta) void {
|
|
588
622
|
for (self.s.excluded.items) |ext| {
|
|
589
|
-
if (ext.len
|
|
623
|
+
if (ext.len == 0) continue;
|
|
624
|
+
if (ext[0] == '/') {
|
|
625
|
+
if (name.len == ext.len - 1 and hasSuffixIgnoreCase(name, ext[1..])) {
|
|
626
|
+
_ = self.s.skipped.fetchAdd(1, .monotonic);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
} else if (hasSuffixIgnoreCase(name, ext)) {
|
|
590
630
|
_ = self.s.skipped.fetchAdd(1, .monotonic);
|
|
591
631
|
return;
|
|
592
632
|
}
|
|
593
633
|
}
|
|
634
|
+
|
|
635
|
+
// ファイル一覧モード(fd 互換): ファイルを開かずにパスのみ記録
|
|
636
|
+
if (self.s.files_only) {
|
|
637
|
+
self.record(.match, job.root, 1, 0, job.path(), name, "");
|
|
638
|
+
if (self.out.len >= LOCAL_FLUSH) self.s.publish(&self.out);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
594
641
|
const ext = extname(name);
|
|
595
642
|
if (ext.len > 0) {
|
|
596
643
|
for (self.s.documents.items) |doc| {
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rufio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- masisz
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: io-console
|