rufio 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +74 -3
  4. data/README_ja.md +71 -2
  5. data/Rakefile +14 -1
  6. data/bin/rufio +31 -2
  7. data/docs/fast_zip_design_ja.md +167 -0
  8. data/info/help.md +2 -0
  9. data/info/keybindings.md +3 -1
  10. data/info/welcome.md +1 -1
  11. data/lib/rufio/application.rb +12 -1
  12. data/lib/rufio/archive_controller.rb +211 -0
  13. data/lib/rufio/background_command_executor.rb +9 -0
  14. data/lib/rufio/background_status.rb +49 -0
  15. data/lib/rufio/bookmark_controller.rb +38 -121
  16. data/lib/rufio/bookmark_manager.rb +77 -331
  17. data/lib/rufio/command_completion.rb +6 -1
  18. data/lib/rufio/command_history.rb +5 -2
  19. data/lib/rufio/command_mode.rb +40 -134
  20. data/lib/rufio/command_mode_ui.rb +4 -42
  21. data/lib/rufio/config.rb +88 -136
  22. data/lib/rufio/config_loader.rb +3 -11
  23. data/lib/rufio/dialog_renderer.rb +64 -152
  24. data/lib/rufio/dialog_support.rb +69 -0
  25. data/lib/rufio/document_text_extractor.rb +167 -0
  26. data/lib/rufio/file_operation_controller.rb +49 -222
  27. data/lib/rufio/file_preview.rb +46 -2
  28. data/lib/rufio/global_search.rb +121 -9
  29. data/lib/rufio/help_menu.rb +8 -2
  30. data/lib/rufio/interpreter_resolver.rb +6 -11
  31. data/lib/rufio/job_manager.rb +2 -5
  32. data/lib/rufio/key_input.rb +77 -0
  33. data/lib/rufio/keybind_handler.rb +81 -115
  34. data/lib/rufio/local_script_scanner.rb +6 -18
  35. data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
  36. data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
  37. data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
  38. data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
  39. data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
  40. data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
  41. data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
  42. data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
  43. data/lib/rufio/native_content_search.rb +153 -0
  44. data/lib/rufio/native_scanner.rb +13 -68
  45. data/lib/rufio/native_scanner_zig.rb +106 -56
  46. data/lib/rufio/native_zip.rb +121 -0
  47. data/lib/rufio/navigation_controller.rb +18 -20
  48. data/lib/rufio/screen.rb +2 -12
  49. data/lib/rufio/script_executor.rb +6 -73
  50. data/lib/rufio/script_file.rb +44 -0
  51. data/lib/rufio/script_path_manager.rb +9 -43
  52. data/lib/rufio/script_runner.rb +13 -50
  53. data/lib/rufio/search_controller.rb +1 -1
  54. data/lib/rufio/shell_command_completion.rb +1 -5
  55. data/lib/rufio/shell_init.rb +90 -0
  56. data/lib/rufio/task_status.rb +5 -1
  57. data/lib/rufio/terminal_ui.rb +55 -294
  58. data/lib/rufio/text_utils.rb +26 -3
  59. data/lib/rufio/ui_renderer.rb +63 -40
  60. data/lib/rufio/version.rb +1 -1
  61. data/lib/rufio/zip_archive.rb +314 -0
  62. data/lib/rufio/zip_extractor.rb +346 -0
  63. data/lib/rufio/zip_writer.rb +268 -0
  64. data/lib/rufio/zoxide_integration.rb +5 -43
  65. data/lib/rufio.rb +5 -0
  66. data/lib_zig/rufio_native/Makefile +54 -17
  67. data/lib_zig/rufio_native/bench.sh +36 -0
  68. data/lib_zig/rufio_native/build.zig +32 -35
  69. data/lib_zig/rufio_native/src/cli.zig +99 -0
  70. data/lib_zig/rufio_native/src/main.zig +187 -83
  71. data/lib_zig/rufio_native/src/os.zig +889 -0
  72. data/lib_zig/rufio_native/src/search.zig +919 -0
  73. data/lib_zig/rufio_native/src/search_test.zig +88 -0
  74. data/lib_zig/rufio_native/src/zip.zig +482 -0
  75. metadata +125 -3
  76. data/lib/rufio/native/rufio_zig.bundle +0 -0
@@ -1,11 +1,8 @@
1
1
  const std = @import("std");
2
- const c = @cImport({
3
- @cInclude("dirent.h");
4
- @cInclude("sys/stat.h");
5
- @cInclude("string.h");
6
- @cInclude("stdlib.h");
7
- @cInclude("pthread.h");
8
- });
2
+ const c = std.c;
3
+ const search = @import("search.zig");
4
+ const os = @import("os.zig");
5
+ const zip = @import("zip.zig");
9
6
 
10
7
  /// スキャン状態
11
8
  pub const ScanState = enum(u8) {
@@ -38,11 +35,10 @@ const AsyncScanner = struct {
38
35
  state: ScanState,
39
36
  progress: usize,
40
37
  total_estimate: usize,
41
- mutex: c.pthread_mutex_t,
38
+ mutex: os.Mutex,
42
39
  path_copy: ?[*:0]u8,
43
40
  max_entries: usize,
44
- thread: c.pthread_t,
45
- thread_started: bool,
41
+ thread: ?std.Thread,
46
42
 
47
43
  fn init() !*AsyncScanner {
48
44
  const self = @as(?*AsyncScanner, @ptrCast(@alignCast(c.malloc(@sizeOf(AsyncScanner))))) orelse return error.OutOfMemory;
@@ -60,19 +56,15 @@ const AsyncScanner = struct {
60
56
  self.*.total_estimate = 0;
61
57
  self.*.path_copy = null;
62
58
  self.*.max_entries = 0;
63
- self.*.thread_started = false;
64
-
65
- // mutex初期化
66
- _ = c.pthread_mutex_init(&self.*.mutex, null);
59
+ self.*.thread = null;
60
+ self.*.mutex = .{};
67
61
 
68
62
  return self;
69
63
  }
70
64
 
71
65
  fn deinit(self: *AsyncScanner) void {
72
66
  // スレッドが実行中なら待機
73
- if (self.thread_started) {
74
- _ = c.pthread_join(self.thread, null);
75
- }
67
+ if (self.thread) |thread| thread.join();
76
68
 
77
69
  // 各エントリの名前を解放
78
70
  for (self.entries[0..self.count]) |*entry| {
@@ -87,9 +79,6 @@ const AsyncScanner = struct {
87
79
  // エントリ配列を解放
88
80
  c.free(self.entries.ptr);
89
81
 
90
- // mutex破棄
91
- _ = c.pthread_mutex_destroy(&self.mutex);
92
-
93
82
  // 自身を解放
94
83
  c.free(self);
95
84
  }
@@ -115,14 +104,14 @@ const AsyncScanner = struct {
115
104
  }
116
105
 
117
106
  fn isCancelled(self: *AsyncScanner) bool {
118
- _ = c.pthread_mutex_lock(&self.mutex);
119
- defer _ = c.pthread_mutex_unlock(&self.mutex);
107
+ self.mutex.lock();
108
+ defer self.mutex.unlock();
120
109
  return self.state == .cancelled;
121
110
  }
122
111
 
123
112
  fn setState(self: *AsyncScanner, new_state: ScanState) void {
124
- _ = c.pthread_mutex_lock(&self.mutex);
125
- defer _ = c.pthread_mutex_unlock(&self.mutex);
113
+ self.mutex.lock();
114
+ defer self.mutex.unlock();
126
115
  self.state = new_state;
127
116
  }
128
117
 
@@ -130,13 +119,13 @@ const AsyncScanner = struct {
130
119
  // クリア
131
120
  self.clear();
132
121
 
133
- const dir = c.opendir(path) orelse return error.CannotOpenDir;
134
- defer _ = c.closedir(dir);
135
-
136
- const path_len = c.strlen(path);
137
- const path_slice = path[0..path_len];
122
+ var scratch: os.Scratch = .{};
123
+ try scratch.init();
124
+ defer scratch.deinit();
125
+ var dir = os.Dir.open(std.mem.span(path), &scratch) orelse return error.CannotOpenDir;
126
+ defer dir.close();
138
127
 
139
- while (c.readdir(dir)) |entry| {
128
+ while (dir.next() catch return error.ReadFailed) |entry| {
140
129
  // キャンセルチェック
141
130
  if (self.isCancelled()) {
142
131
  return error.Cancelled;
@@ -145,63 +134,48 @@ const AsyncScanner = struct {
145
134
  // エントリ数制限チェック
146
135
  if (max_entries > 0 and self.count >= max_entries) break;
147
136
 
148
- const name_ptr = @as([*:0]const u8, @ptrCast(&entry.*.d_name));
149
- const name_len = c.strlen(name_ptr);
150
- const name_slice = name_ptr[0..name_len];
151
-
152
- // "." と ".." をスキップ
153
- if (std.mem.eql(u8, name_slice, ".") or std.mem.eql(u8, name_slice, "..")) {
154
- continue;
155
- }
137
+ // ファイル情報を取得(シンボリックリンクは辿らない)
138
+ const info = dir.currentInfo(entry) orelse continue;
156
139
 
157
140
  // 容量確保
158
141
  try self.ensureCapacity(self.count + 1);
159
142
 
160
- // フルパスを構築
161
- var path_buf: [std.fs.max_path_bytes]u8 = undefined;
162
- const full_path = std.fmt.bufPrintZ(&path_buf, "{s}/{s}", .{ path_slice, name_slice }) catch continue;
163
-
164
- // ファイル情報を取得
165
- var stat_buf: c.struct_stat = undefined;
166
- if (c.lstat(full_path.ptr, &stat_buf) != 0) {
167
- continue;
168
- }
169
-
170
143
  // 名前をコピー
144
+ const name_len = entry.name.len;
171
145
  const name_copy_ptr = c.malloc(name_len + 1) orelse return error.OutOfMemory;
172
146
  const name_copy_bytes = @as([*]u8, @ptrCast(@alignCast(name_copy_ptr)));
173
147
  const name_copy = name_copy_bytes[0..name_len];
174
- @memcpy(name_copy, name_slice);
148
+ @memcpy(name_copy, entry.name);
175
149
  name_copy_bytes[name_len] = 0;
176
150
 
177
151
  // エントリを追加
178
152
  self.entries[self.count] = DirEntry{
179
153
  .name = name_copy,
180
- .is_dir = (stat_buf.st_mode & c.S_IFMT) == c.S_IFDIR,
181
- .size = @intCast(stat_buf.st_size),
182
- .mtime = @intCast(stat_buf.st_mtimespec.tv_sec),
183
- .executable = (stat_buf.st_mode & c.S_IXUSR) != 0,
184
- .hidden = name_slice[0] == '.',
154
+ .is_dir = info.kind == .dir,
155
+ .size = info.size,
156
+ .mtime = info.mtime_sec,
157
+ .executable = info.executable,
158
+ .hidden = entry.name[0] == '.',
185
159
  };
186
160
  self.count += 1;
187
161
 
188
162
  // 進捗更新
189
- _ = c.pthread_mutex_lock(&self.mutex);
163
+ self.mutex.lock();
190
164
  self.progress = self.count;
191
- _ = c.pthread_mutex_unlock(&self.mutex);
165
+ self.mutex.unlock();
192
166
  }
193
167
  }
194
168
 
195
169
  fn scanAsync(self: *AsyncScanner, path: [*:0]const u8, max_entries: usize) !void {
196
- _ = c.pthread_mutex_lock(&self.mutex);
197
- defer _ = c.pthread_mutex_unlock(&self.mutex);
170
+ self.mutex.lock();
171
+ defer self.mutex.unlock();
198
172
 
199
173
  if (self.state == .scanning) {
200
174
  return error.AlreadyScanning;
201
175
  }
202
176
 
203
177
  // パスをコピー
204
- const path_len = c.strlen(path);
178
+ const path_len = std.mem.len(path);
205
179
  const path_copy_ptr = c.malloc(path_len + 1) orelse return error.OutOfMemory;
206
180
  const path_copy_bytes = @as([*:0]u8, @ptrCast(@alignCast(path_copy_ptr)));
207
181
  @memcpy(path_copy_bytes[0..path_len], path[0..path_len]);
@@ -216,13 +190,13 @@ const AsyncScanner = struct {
216
190
  self.max_entries = max_entries;
217
191
  self.state = .scanning;
218
192
 
219
- // スレッド開始
220
- const result = c.pthread_create(&self.thread, null, scanWorkerEntry, self);
221
- if (result != 0) {
193
+ // 前回のスキャンスレッドは完了済みなので回収してから開始
194
+ if (self.thread) |thread| thread.join();
195
+ self.thread = std.Thread.spawn(.{}, scanWorker, .{self}) catch {
196
+ self.thread = null;
222
197
  self.state = .failed;
223
198
  return error.ThreadCreateFailed;
224
- }
225
- self.thread_started = true;
199
+ };
226
200
  }
227
201
 
228
202
  fn scanWorker(self: *AsyncScanner) void {
@@ -244,13 +218,6 @@ const AsyncScanner = struct {
244
218
  }
245
219
  };
246
220
 
247
- // スレッドエントリポイント
248
- fn scanWorkerEntry(arg: ?*anyopaque) callconv(.c) ?*anyopaque {
249
- const scanner = @as(*AsyncScanner, @ptrCast(@alignCast(arg)));
250
- scanner.scanWorker();
251
- return null;
252
- }
253
-
254
221
  // ============================================================
255
222
  // ABI Boundary: Ruby ABI非依存・ハンドルベース
256
223
  // ============================================================
@@ -284,8 +251,8 @@ export fn core_async_get_state(handle: u64) u8 {
284
251
  if (handle == 0) return @intFromEnum(ScanState.failed);
285
252
 
286
253
  const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
287
- _ = c.pthread_mutex_lock(&scanner.mutex);
288
- defer _ = c.pthread_mutex_unlock(&scanner.mutex);
254
+ scanner.mutex.lock();
255
+ defer scanner.mutex.unlock();
289
256
  return @intFromEnum(scanner.state);
290
257
  }
291
258
 
@@ -298,8 +265,8 @@ export fn core_async_get_progress(handle: u64, current: *usize, total: *usize) v
298
265
  }
299
266
 
300
267
  const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
301
- _ = c.pthread_mutex_lock(&scanner.mutex);
302
- defer _ = c.pthread_mutex_unlock(&scanner.mutex);
268
+ scanner.mutex.lock();
269
+ defer scanner.mutex.unlock();
303
270
 
304
271
  current.* = scanner.progress;
305
272
  total.* = scanner.total_estimate;
@@ -310,8 +277,8 @@ export fn core_async_cancel(handle: u64) void {
310
277
  if (handle == 0) return;
311
278
 
312
279
  const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
313
- _ = c.pthread_mutex_lock(&scanner.mutex);
314
- defer _ = c.pthread_mutex_unlock(&scanner.mutex);
280
+ scanner.mutex.lock();
281
+ defer scanner.mutex.unlock();
315
282
 
316
283
  if (scanner.state == .scanning) {
317
284
  scanner.state = .cancelled;
@@ -323,8 +290,8 @@ export fn core_async_get_count(handle: u64) usize {
323
290
  if (handle == 0) return 0;
324
291
 
325
292
  const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
326
- _ = c.pthread_mutex_lock(&scanner.mutex);
327
- defer _ = c.pthread_mutex_unlock(&scanner.mutex);
293
+ scanner.mutex.lock();
294
+ defer scanner.mutex.unlock();
328
295
  return scanner.count;
329
296
  }
330
297
 
@@ -333,8 +300,8 @@ export fn core_async_get_name(handle: u64, index: usize, buf: [*]u8, buf_size: u
333
300
  if (handle == 0) return 0;
334
301
 
335
302
  const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
336
- _ = c.pthread_mutex_lock(&scanner.mutex);
337
- defer _ = c.pthread_mutex_unlock(&scanner.mutex);
303
+ scanner.mutex.lock();
304
+ defer scanner.mutex.unlock();
338
305
 
339
306
  if (index >= scanner.count) return 0;
340
307
 
@@ -350,8 +317,8 @@ export fn core_async_get_attrs(handle: u64, index: usize, is_dir: *u8, size: *u6
350
317
  if (handle == 0) return -1;
351
318
 
352
319
  const scanner = @as(*AsyncScanner, @ptrFromInt(handle));
353
- _ = c.pthread_mutex_lock(&scanner.mutex);
354
- defer _ = c.pthread_mutex_unlock(&scanner.mutex);
320
+ scanner.mutex.lock();
321
+ defer scanner.mutex.unlock();
355
322
 
356
323
  if (index >= scanner.count) return -1;
357
324
 
@@ -376,3 +343,140 @@ export fn core_async_destroy(handle: u64) void {
376
343
  export fn core_async_version() [*:0]const u8 {
377
344
  return "4.0.0-async";
378
345
  }
346
+
347
+ // ============================================================
348
+ // コンテンツ検索(G mode): NUL 区切りのリストを長さ付きで受け取る
349
+ // ============================================================
350
+
351
+ /// 検索を開始してハンドルを返す(失敗時は 0)。threads=0 で CPU 数
352
+ export fn core_search_create(
353
+ roots: [*]const u8,
354
+ roots_len: usize,
355
+ query: [*]const u8,
356
+ query_len: usize,
357
+ excluded: [*]const u8,
358
+ excluded_len: usize,
359
+ documents: [*]const u8,
360
+ documents_len: usize,
361
+ threads: u32,
362
+ cache_handle: u64,
363
+ ) u64 {
364
+ const cache: ?*search.Cache = if (cache_handle == 0) null else @ptrFromInt(cache_handle);
365
+ const s = search.Search.create(
366
+ roots[0..roots_len],
367
+ query[0..query_len],
368
+ excluded[0..excluded_len],
369
+ documents[0..documents_len],
370
+ threads,
371
+ cache,
372
+ ) catch return 0;
373
+ return @intFromPtr(s);
374
+ }
375
+
376
+ export fn core_search_state(handle: u64) u8 {
377
+ if (handle == 0) return @intFromEnum(search.State.failed);
378
+ const s: *search.Search = @ptrFromInt(handle);
379
+ return @intFromEnum(s.getState());
380
+ }
381
+
382
+ /// 溜まったレコードを取り出す。返したポインタは次の drain/destroy まで有効
383
+ export fn core_search_drain(handle: u64, out_len: *usize) ?[*]const u8 {
384
+ out_len.* = 0;
385
+ if (handle == 0) return null;
386
+ const s: *search.Search = @ptrFromInt(handle);
387
+ const data = s.drain();
388
+ out_len.* = data.len;
389
+ return if (data.len == 0) null else data.ptr;
390
+ }
391
+
392
+ export fn core_search_skipped(handle: u64) u64 {
393
+ if (handle == 0) return 0;
394
+ const s: *search.Search = @ptrFromInt(handle);
395
+ return s.skipped.load(.acquire);
396
+ }
397
+
398
+ export fn core_search_cancel(handle: u64) void {
399
+ if (handle == 0) return;
400
+ const s: *search.Search = @ptrFromInt(handle);
401
+ s.cancel();
402
+ }
403
+
404
+ export fn core_search_destroy(handle: u64) void {
405
+ if (handle == 0) return;
406
+ const s: *search.Search = @ptrFromInt(handle);
407
+ s.destroy();
408
+ }
409
+
410
+ /// 検索間で共有するファイル内容キャッシュ(max_bytes はキャッシュする内容の上限)
411
+ export fn core_cache_create(max_bytes: u64) u64 {
412
+ const cache = search.Cache.create(max_bytes) catch return 0;
413
+ return @intFromPtr(cache);
414
+ }
415
+
416
+ export fn core_cache_bytes(handle: u64) u64 {
417
+ if (handle == 0) return 0;
418
+ const cache: *search.Cache = @ptrFromInt(handle);
419
+ return cache.cachedBytes();
420
+ }
421
+
422
+ /// 使用中の検索をすべて destroy してから呼ぶこと
423
+ export fn core_cache_destroy(handle: u64) void {
424
+ if (handle == 0) return;
425
+ const cache: *search.Cache = @ptrFromInt(handle);
426
+ cache.destroy();
427
+ }
428
+
429
+ // ============================================================
430
+ // ZIP データ処理(展開・圧縮): 検証やパスの決定は Ruby 側で行う
431
+ // ============================================================
432
+
433
+ /// 展開ジョブを開始してハンドルを返す(失敗時は 0)。threads=0 で CPU 数
434
+ export fn core_zip_extract_create(zip_path: [*]const u8, zip_path_len: usize, tasks: [*]const u8, tasks_len: usize, threads: u32) u64 {
435
+ const job = zip.Job.createExtract(zip_path[0..zip_path_len], tasks[0..tasks_len], threads) catch return 0;
436
+ return @intFromPtr(job);
437
+ }
438
+
439
+ /// 圧縮ジョブを開始してハンドルを返す(失敗時は 0)。level は 1〜9(それ以外は既定)
440
+ export fn core_zip_deflate_create(tasks: [*]const u8, tasks_len: usize, level: i32, threads: u32) u64 {
441
+ const job = zip.Job.createDeflate(tasks[0..tasks_len], level, threads) catch return 0;
442
+ return @intFromPtr(job);
443
+ }
444
+
445
+ export fn core_zip_state(handle: u64) u8 {
446
+ if (handle == 0) return @intFromEnum(zip.State.failed);
447
+ const job: *zip.Job = @ptrFromInt(handle);
448
+ return @intFromEnum(job.getState());
449
+ }
450
+
451
+ export fn core_zip_progress(handle: u64, done: *u64, total: *u64) void {
452
+ done.* = 0;
453
+ total.* = 0;
454
+ if (handle == 0) return;
455
+ const job: *zip.Job = @ptrFromInt(handle);
456
+ job.progress(done, total);
457
+ }
458
+
459
+ export fn core_zip_status(handle: u64, index: usize) u8 {
460
+ if (handle == 0) return @intFromEnum(zip.Status.io_error);
461
+ const job: *zip.Job = @ptrFromInt(handle);
462
+ return @intFromEnum(job.status(index));
463
+ }
464
+
465
+ export fn core_zip_deflate_result(handle: u64, index: usize, crc: *u32, compressed: *u64, size: *u64) u8 {
466
+ if (handle == 0) return @intFromEnum(zip.Status.io_error);
467
+ const job: *zip.Job = @ptrFromInt(handle);
468
+ return @intFromEnum(job.deflateResult(index, crc, compressed, size));
469
+ }
470
+
471
+ export fn core_zip_cancel(handle: u64) void {
472
+ if (handle == 0) return;
473
+ const job: *zip.Job = @ptrFromInt(handle);
474
+ job.cancel();
475
+ }
476
+
477
+ /// 実行中ならキャンセルしてスレッドの終了を待つ
478
+ export fn core_zip_destroy(handle: u64) void {
479
+ if (handle == 0) return;
480
+ const job: *zip.Job = @ptrFromInt(handle);
481
+ job.destroy();
482
+ }