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
data/lib/rufio.rb CHANGED
@@ -4,6 +4,8 @@ require_relative 'rufio/version'
4
4
  require_relative 'rufio/config'
5
5
  require_relative 'rufio/config_loader'
6
6
  require_relative 'rufio/color_helper'
7
+ require_relative 'rufio/key_input'
8
+ require_relative 'rufio/shell_init'
7
9
  require_relative 'rufio/directory_listing'
8
10
  require_relative 'rufio/filter_manager'
9
11
  require_relative 'rufio/selection_manager'
@@ -22,6 +24,9 @@ require_relative 'rufio/search_controller'
22
24
  require_relative 'rufio/keybind_handler'
23
25
  require_relative 'rufio/ansi_line_parser'
24
26
  require_relative 'rufio/syntax_highlighter'
27
+ require_relative 'rufio/zip_archive'
28
+ require_relative 'rufio/zip_extractor'
29
+ require_relative 'rufio/zip_writer'
25
30
  require_relative 'rufio/file_preview'
26
31
  require_relative 'rufio/terminal_ui'
27
32
  require_relative 'rufio/application'
@@ -1,34 +1,71 @@
1
- # Makefile for Zig Ruby extension
1
+ # Makefile for Zig native library (Ruby ABI 非依存・Fiddle 経由でロード)
2
+ #
3
+ # make 現在の環境向けにビルド
4
+ # make install 現在の環境向けだけを Gem 同梱と同じ設定でビルドして lib/rufio/native/<platform>/ に配置
5
+ # make dist Gem 同梱用に全プラットフォームをクロスコンパイル
6
+ # make test 検索エンジンの単体テスト
2
7
 
3
- RUBY := ruby
4
8
  ZIG := zig
5
-
6
- # Rubyの設定を取得
7
- RUBY_INCLUDE := $(shell $(RUBY) -e "puts RbConfig::CONFIG['rubyhdrdir']")
8
- RUBY_ARCH_INCLUDE := $(shell $(RUBY) -e "puts RbConfig::CONFIG['rubyarchhdrdir']")
9
- RUBY_LIB_DIR := $(shell $(RUBY) -e "puts RbConfig::CONFIG['libdir']")
10
- RUBY_VERSION := $(shell $(RUBY) -e "puts RbConfig::CONFIG['MAJOR'] + '.' + RbConfig::CONFIG['MINOR']")
9
+ RUBY := ruby
11
10
 
12
11
  # コンパイルフラグ
13
12
  ZIG_FLAGS := -O ReleaseFast -dynamic -lc
14
- ZIG_FLAGS += -I$(RUBY_INCLUDE)
15
- ZIG_FLAGS += -I$(RUBY_ARCH_INCLUDE)
16
- ZIG_FLAGS += -L$(RUBY_LIB_DIR)
17
- ZIG_FLAGS += -lruby.$(RUBY_VERSION)
18
13
 
19
14
  # ターゲット
20
15
  TARGET := librufio_zig.dylib
16
+ BENCH := rufio_search
21
17
  SRC := src/main.zig
18
+ SOURCES := src/main.zig src/search.zig src/os.zig src/zip.zig
19
+ NATIVE_DIR := ../../lib/rufio/native
20
+ BUILD_DIR := zig-out/dist
21
+
22
+ # Gem 同梱プラットフォーム(ディレクトリ名:Zig ターゲット:ファイル名)
23
+ # Linux は glibc 2.17(CentOS 7 世代)以降で動くようにリンクする
24
+ PLATFORMS := \
25
+ aarch64-macos:aarch64-macos.11.0:librufio_zig.dylib \
26
+ x86_64-macos:x86_64-macos.10.15:librufio_zig.dylib \
27
+ x86_64-linux-gnu:x86_64-linux-gnu.2.17:librufio_zig.so \
28
+ aarch64-linux-gnu:aarch64-linux-gnu.2.17:librufio_zig.so \
29
+ x86_64-linux-musl:x86_64-linux-musl:librufio_zig.so \
30
+ aarch64-linux-musl:aarch64-linux-musl:librufio_zig.so \
31
+ x86_64-windows:x86_64-windows-gnu:rufio_zig.dll \
32
+ aarch64-windows:aarch64-windows-gnu:rufio_zig.dll
22
33
 
23
34
  all: $(TARGET)
24
35
 
25
- $(TARGET): $(SRC)
36
+ $(TARGET): $(SOURCES)
26
37
  $(ZIG) build-lib $(SRC) $(ZIG_FLAGS) -femit-bin=$@
27
38
 
39
+ # ripgrep との比較用 CLI
40
+ $(BENCH): src/cli.zig src/search.zig src/os.zig
41
+ $(ZIG) build-exe src/cli.zig -O ReleaseFast -lc -femit-bin=$@
42
+
43
+ test:
44
+ $(ZIG) test src/search_test.zig
45
+
46
+ bench: $(BENCH)
47
+ ./bench.sh
48
+
49
+ # ONLY=<platform> で 1 つだけビルドする
50
+ dist: $(SOURCES)
51
+ @for entry in $(PLATFORMS); do \
52
+ dir=$${entry%%:*}; rest=$${entry#*:}; triple=$${rest%%:*}; file=$${rest#*:}; \
53
+ if [ -n "$(ONLY)" ] && [ "$(ONLY)" != "$$dir" ]; then continue; fi; \
54
+ echo "==> $$dir ($$triple)"; \
55
+ mkdir -p $(BUILD_DIR)/$$dir $(NATIVE_DIR)/$$dir || exit 1; \
56
+ $(ZIG) build-lib $(SRC) -target $$triple $(ZIG_FLAGS) -fstrip \
57
+ -femit-bin=$(BUILD_DIR)/$$dir/$$file || exit 1; \
58
+ cp $(BUILD_DIR)/$$dir/$$file $(NATIVE_DIR)/$$dir/$$file || exit 1; \
59
+ done
60
+
28
61
  clean:
29
- rm -f $(TARGET) *.o
62
+ rm -rf $(TARGET) $(BENCH) *.o $(BUILD_DIR)
30
63
 
31
- install: $(TARGET)
32
- cp $(TARGET) ../../lib/rufio/native/rufio_zig.bundle
64
+ # 現在の環境のプラットフォーム名は Ruby 側の判定(NativeScannerZigFFI.platform_dir)に合わせ、
65
+ # Gem 同梱と同じ設定でビルドする
66
+ install:
67
+ @platform=$$($(RUBY) -I../../lib -rrufio/native_scanner -rrufio/native_scanner_zig -e 'print Rufio::NativeScannerZigFFI.platform_dir.to_s' 2>/dev/null); \
68
+ if [ -z "$$platform" ]; then echo "unsupported platform"; exit 1; fi; \
69
+ $(MAKE) --no-print-directory dist ONLY=$$platform
33
70
 
34
- .PHONY: all clean install
71
+ .PHONY: all clean install test bench dist
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # rufio_search と ripgrep の速度比較(ウォームキャッシュ、各 10 回の中央値)
3
+ # 使い方: ./bench.sh [DIR] [QUERY...]
4
+ set -euo pipefail
5
+ cd "$(dirname "$0")"
6
+
7
+ DIR=${1:-$(cd ../.. && pwd)}
8
+ shift || true
9
+ QUERIES=("$@")
10
+ [ ${#QUERIES[@]} -eq 0 ] && QUERIES=(rb_define_method zzqxj_not_found "the" "日本語")
11
+ RUNS=${RUNS:-10}
12
+
13
+ [ -x ./rufio_search ] || make rufio_search >/dev/null
14
+
15
+ measure() {
16
+ python3 - "$RUNS" "$@" <<'EOF'
17
+ import subprocess, sys, time, statistics
18
+ runs = int(sys.argv[1]); cmd = sys.argv[2:]
19
+ for _ in range(2):
20
+ subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
21
+ times = []
22
+ for _ in range(runs):
23
+ t = time.perf_counter()
24
+ subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
25
+ times.append((time.perf_counter() - t) * 1000)
26
+ print(f"median {statistics.median(times):8.1f} ms min {min(times):8.1f} ms")
27
+ EOF
28
+ }
29
+
30
+ echo "corpus: $DIR ($(find "$DIR" -type f | wc -l | tr -d ' ') files, $(du -sh "$DIR" | cut -f1))"
31
+ for q in "${QUERIES[@]}"; do
32
+ hits=$(./rufio_search "$q" "$DIR" | wc -l | tr -d ' ')
33
+ echo "== query: $q ($hits lines)"
34
+ printf ' rufio_search : '; measure ./rufio_search "$q" "$DIR"
35
+ printf ' rg -uu -i -F : '; measure rg -uu -i -F -n "$q" "$DIR"
36
+ done
@@ -4,42 +4,39 @@ pub fn build(b: *std.Build) void {
4
4
  const target = b.standardTargetOptions(.{});
5
5
  const optimize = b.standardOptimizeOption(.{});
6
6
 
7
- // 動的ライブラリを作成
8
- const lib = b.addExecutable(.{
7
+ // 動的ライブラリ(Ruby ABI 非依存・Fiddle 経由でロード)
8
+ const lib = b.addLibrary(.{
9
9
  .name = "rufio_zig",
10
- .root_source_file = b.path("src/main.zig"),
11
- .target = target,
12
- .optimize = optimize,
10
+ .linkage = .dynamic,
11
+ .root_module = b.createModule(.{
12
+ .root_source_file = b.path("src/main.zig"),
13
+ .target = target,
14
+ .optimize = optimize,
15
+ .link_libc = true,
16
+ }),
13
17
  });
14
-
15
- // 共有ライブラリとしてリンク
16
- lib.linkage = .dynamic;
17
- lib.linkLibC();
18
-
19
- // RubyのヘッダーとライブラリへのパスをRubyから取得
20
- var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
21
- defer arena.deinit();
22
- const allocator = arena.allocator();
23
-
24
- // Rubyのインクルードパスを取得
25
- const ruby_include_result = b.run(&.{ "ruby", "-e", "puts RbConfig::CONFIG['rubyhdrdir']" });
26
- const ruby_include = std.mem.trim(u8, ruby_include_result, " \n\r\t");
27
-
28
- const ruby_arch_include_result = b.run(&.{ "ruby", "-e", "puts RbConfig::CONFIG['rubyarchhdrdir']" });
29
- const ruby_arch_include = std.mem.trim(u8, ruby_arch_include_result, " \n\r\t");
30
-
31
- const ruby_lib_dir_result = b.run(&.{ "ruby", "-e", "puts RbConfig::CONFIG['libdir']" });
32
- const ruby_lib_dir = std.mem.trim(u8, ruby_lib_dir_result, " \n\r\t");
33
-
34
- // インクルードパスを追加
35
- lib.addIncludePath(.{ .cwd_relative = ruby_include });
36
- lib.addIncludePath(.{ .cwd_relative = ruby_arch_include });
37
-
38
- // ライブラリパスを追加
39
- lib.addLibraryPath(.{ .cwd_relative = ruby_lib_dir });
40
-
41
- // Rubyライブラリとリンク
42
- lib.linkSystemLibrary("ruby.3.4");
43
-
44
18
  b.installArtifact(lib);
19
+
20
+ // ripgrep との比較用 CLI
21
+ const cli = b.addExecutable(.{
22
+ .name = "rufio_search",
23
+ .root_module = b.createModule(.{
24
+ .root_source_file = b.path("src/cli.zig"),
25
+ .target = target,
26
+ .optimize = optimize,
27
+ .link_libc = true,
28
+ }),
29
+ });
30
+ b.installArtifact(cli);
31
+
32
+ const tests = b.addTest(.{
33
+ .root_module = b.createModule(.{
34
+ .root_source_file = b.path("src/search_test.zig"),
35
+ .target = target,
36
+ .optimize = optimize,
37
+ .link_libc = true,
38
+ }),
39
+ });
40
+ const test_step = b.step("test", "検索エンジンの単体テストを実行");
41
+ test_step.dependOn(&b.addRunArtifact(tests).step);
45
42
  }
@@ -0,0 +1,99 @@
1
+ //! ripgrep と同条件で比較するためのベンチマーク用 CLI。
2
+ //! 使い方: rufio_search [-j N] [-r N] QUERY DIR...
3
+ //! -r N: キャッシュを使って同じ検索を N 回繰り返し、各回の所要時間を標準エラーに出す(G mode の再検索を再現)
4
+ //! 出力は rg -n と同じ `path:line:text` 形式(text はマッチ周辺の最大 1KB)。
5
+ const std = @import("std");
6
+ const c = std.c;
7
+ const search = @import("search.zig");
8
+ const os = @import("os.zig");
9
+
10
+ fn writeAll(bytes: []const u8) void {
11
+ os.writeAll(1, bytes);
12
+ }
13
+
14
+ pub fn main(init: std.process.Init.Minimal) !void {
15
+ var args = try init.args.iterateAllocator(std.heap.c_allocator);
16
+ defer args.deinit();
17
+ _ = args.skip();
18
+ var threads: usize = 0;
19
+ var repeat: usize = 0;
20
+ var query: ?[]const u8 = null;
21
+ var roots: std.ArrayList(u8) = .empty;
22
+ const alloc = std.heap.c_allocator;
23
+ defer roots.deinit(alloc);
24
+
25
+ while (args.next()) |arg| {
26
+ if (std.mem.eql(u8, arg, "-j")) {
27
+ threads = try std.fmt.parseInt(usize, args.next() orelse return error.MissingValue, 10);
28
+ } else if (std.mem.eql(u8, arg, "-r")) {
29
+ repeat = try std.fmt.parseInt(usize, args.next() orelse return error.MissingValue, 10);
30
+ } else if (query == null) {
31
+ query = arg;
32
+ } else {
33
+ if (roots.items.len > 0) try roots.append(alloc, 0);
34
+ try roots.appendSlice(alloc, std.mem.trimEnd(u8, arg, "/"));
35
+ }
36
+ }
37
+ const q = query orelse {
38
+ writeAll("usage: rufio_search [-j N] [-r N] QUERY DIR...\n");
39
+ return;
40
+ };
41
+ if (roots.items.len == 0) try roots.append(alloc, '.');
42
+
43
+ if (repeat > 0) {
44
+ const cache = try search.Cache.create(256 * 1024 * 1024);
45
+ defer cache.destroy();
46
+ var i: usize = 0;
47
+ while (i < repeat) : (i += 1) {
48
+ const t0 = os.nowNs();
49
+ const lines = try runSearch(roots.items, q, threads, cache, i + 1 == repeat);
50
+ const ms = @as(f64, @floatFromInt(os.nowNs() - t0)) / 1e6;
51
+ var msg: [128]u8 = undefined;
52
+ os.writeAll(2, try std.fmt.bufPrint(&msg, "run {d}: {d:.1} ms, {d} lines, cache {d} MB\n", .{ i + 1, ms, lines, cache.cachedBytes() >> 20 }));
53
+ }
54
+ return;
55
+ }
56
+ _ = try runSearch(roots.items, q, threads, null, true);
57
+ }
58
+
59
+ fn runSearch(roots: []const u8, q: []const u8, threads: usize, cache: ?*search.Cache, print: bool) !usize {
60
+ const alloc = std.heap.c_allocator;
61
+ const s = try search.Search.create(roots, q, "", "", threads, cache);
62
+ defer s.destroy();
63
+
64
+ var lines: usize = 0;
65
+ var out: std.ArrayList(u8) = .empty;
66
+ defer out.deinit(alloc);
67
+ var line_buf: [24]u8 = undefined;
68
+ while (true) {
69
+ const state = s.getState();
70
+ const data = s.drain();
71
+ var pos: usize = 0;
72
+ while (pos + search.RECORD_HEADER <= data.len) {
73
+ const kind = data[pos];
74
+ const line = std.mem.readInt(u64, data[pos + 5 ..][0..8], .little);
75
+ const a_len = std.mem.readInt(u32, data[pos + 17 ..][0..4], .little);
76
+ const b_len = std.mem.readInt(u32, data[pos + 21 ..][0..4], .little);
77
+ const a = data[pos + search.RECORD_HEADER ..][0..a_len];
78
+ const b = data[pos + search.RECORD_HEADER + a_len ..][0..b_len];
79
+ pos += search.RECORD_HEADER + a_len + b_len;
80
+ if (kind != @intFromEnum(search.Kind.match)) continue;
81
+ lines += 1;
82
+ if (!print) continue;
83
+ try out.appendSlice(alloc, a);
84
+ try out.appendSlice(alloc, try std.fmt.bufPrint(&line_buf, ":{d}:", .{line}));
85
+ try out.appendSlice(alloc, std.mem.trimEnd(u8, b, "\r\n"));
86
+ try out.append(alloc, '\n');
87
+ }
88
+ if (out.items.len >= 1 << 16) {
89
+ writeAll(out.items);
90
+ out.clearRetainingCapacity();
91
+ }
92
+ if (data.len == 0) {
93
+ if (state != .scanning) break;
94
+ os.sleepMicros(50);
95
+ }
96
+ }
97
+ writeAll(out.items);
98
+ return lines;
99
+ }