rufio 1.1.0 → 1.4.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 +50 -0
- data/README.md +26 -297
- data/README_ja.md +24 -261
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/docs/guide.md +410 -0
- data/docs/guide_ja.md +410 -0
- data/info/help.md +54 -17
- data/info/keybindings.md +50 -22
- data/info/welcome.md +14 -5
- data/lib/rufio/application.rb +12 -1
- data/lib/rufio/archive_controller.rb +211 -0
- data/lib/rufio/background_command_executor.rb +9 -0
- data/lib/rufio/background_status.rb +49 -0
- data/lib/rufio/bookmark_controller.rb +38 -121
- data/lib/rufio/bookmark_manager.rb +77 -331
- data/lib/rufio/command_completion.rb +28 -2
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +60 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +4 -11
- data/lib/rufio/dialog_renderer.rb +90 -152
- data/lib/rufio/dialog_support.rb +69 -0
- data/lib/rufio/document_text_extractor.rb +167 -0
- data/lib/rufio/file_operation_controller.rb +49 -222
- data/lib/rufio/file_operations.rb +28 -6
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +125 -13
- data/lib/rufio/help_menu.rb +19 -7
- data/lib/rufio/interpreter_resolver.rb +6 -11
- data/lib/rufio/job_manager.rb +2 -5
- data/lib/rufio/key_input.rb +77 -0
- data/lib/rufio/keybind_handler.rb +83 -116
- data/lib/rufio/local_script_scanner.rb +6 -18
- 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 +153 -0
- data/lib/rufio/native_scanner.rb +13 -68
- data/lib/rufio/native_scanner_zig.rb +106 -56
- data/lib/rufio/native_zip.rb +121 -0
- data/lib/rufio/navigation_controller.rb +20 -26
- data/lib/rufio/path_jump.rb +55 -0
- data/lib/rufio/screen.rb +2 -12
- data/lib/rufio/script_executor.rb +6 -73
- data/lib/rufio/script_file.rb +44 -0
- data/lib/rufio/script_path_manager.rb +9 -43
- data/lib/rufio/script_runner.rb +13 -50
- data/lib/rufio/search_controller.rb +1 -1
- data/lib/rufio/shell_command_completion.rb +1 -5
- data/lib/rufio/shell_init.rb +90 -0
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +84 -298
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +73 -45
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio/zip_archive.rb +314 -0
- data/lib/rufio/zip_extractor.rb +346 -0
- data/lib/rufio/zip_writer.rb +268 -0
- data/lib/rufio/zoxide_integration.rb +5 -43
- data/lib/rufio.rb +6 -0
- data/lib_zig/rufio_native/Makefile +54 -17
- data/lib_zig/rufio_native/bench.sh +36 -0
- data/lib_zig/rufio_native/build.zig +32 -35
- data/lib_zig/rufio_native/src/cli.zig +99 -0
- data/lib_zig/rufio_native/src/main.zig +187 -83
- data/lib_zig/rufio_native/src/os.zig +889 -0
- data/lib_zig/rufio_native/src/search.zig +919 -0
- data/lib_zig/rufio_native/src/search_test.zig +88 -0
- data/lib_zig/rufio_native/src/zip.zig +482 -0
- metadata +128 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
data/lib/rufio/global_search.rb
CHANGED
|
@@ -3,19 +3,36 @@
|
|
|
3
3
|
require 'thread'
|
|
4
4
|
require_relative 'text_utils'
|
|
5
5
|
require_relative 'config_loader'
|
|
6
|
+
require_relative 'document_text_extractor'
|
|
7
|
+
require_relative 'native_content_search'
|
|
6
8
|
|
|
7
9
|
module Rufio
|
|
8
10
|
# ブックマーク配下のファイル名・テキスト内容をバックグラウンドで横断検索する。
|
|
9
11
|
class GlobalSearch
|
|
10
12
|
SPINNER_FRAMES = ['|', '/', '-', '\\'].freeze
|
|
11
13
|
SPINNER_INTERVAL = 0.12
|
|
14
|
+
# 検索語に一致した部分の表示(黒の太字・黄色背景)
|
|
15
|
+
MATCH_FG = "\e[30m\e[1m"
|
|
16
|
+
MATCH_BG = "\e[43m"
|
|
12
17
|
|
|
13
18
|
attr_reader :query, :results, :errors, :selected_index, :search_type, :skipped_files
|
|
14
19
|
|
|
20
|
+
class << self
|
|
21
|
+
# nil: 利用可能なら Zig 実装を使う(RUFIO_NATIVE_SEARCH=0 で無効化)
|
|
22
|
+
attr_writer :native_search
|
|
23
|
+
|
|
24
|
+
def native_search?
|
|
25
|
+
enabled = @native_search.nil? ? ENV['RUFIO_NATIVE_SEARCH'] != '0' : @native_search
|
|
26
|
+
enabled && NativeContentSearch.available?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
15
30
|
def initialize
|
|
16
31
|
@active = false
|
|
17
32
|
@scanning = false
|
|
18
33
|
@worker = nil
|
|
34
|
+
@document_extractor = DocumentTextExtractor.new
|
|
35
|
+
@document_cache = DocumentTextCache.new(extractor: @document_extractor)
|
|
19
36
|
end
|
|
20
37
|
|
|
21
38
|
def active?
|
|
@@ -63,8 +80,12 @@ module Rufio
|
|
|
63
80
|
when :entry
|
|
64
81
|
@entries << value
|
|
65
82
|
@results << value if @search_type == :content || matches_query?(value)
|
|
83
|
+
when :entries
|
|
84
|
+
# Zig 実装の結果はまとめて届くので、1 フレーム 256 件の上限に縛られない
|
|
85
|
+
@entries.concat(value)
|
|
86
|
+
@results.concat(value)
|
|
66
87
|
when :error then @errors << value
|
|
67
|
-
when :skipped then @skipped_files += 1
|
|
88
|
+
when :skipped then @skipped_files += value || 1
|
|
68
89
|
when :done then @scanning = false
|
|
69
90
|
end
|
|
70
91
|
end
|
|
@@ -85,11 +106,11 @@ module Rufio
|
|
|
85
106
|
when "\t"
|
|
86
107
|
@search_type = @search_type == :filename ? :content : :filename
|
|
87
108
|
start_scan
|
|
88
|
-
when "\r"
|
|
109
|
+
when "\r" then return @results[@selected_index]
|
|
89
110
|
when :down then @selected_index = [@selected_index + 1, [@results.length - 1, 0].max].min
|
|
90
111
|
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
|
|
112
|
+
when "\x0a", "\x0e" then handle_key(:down) # Ctrl+J / Ctrl+N
|
|
113
|
+
when "\x0b", "\x10" then handle_key(:up) # Ctrl+K / Ctrl+P
|
|
93
114
|
when "\x15" then update_query('') # Ctrl+U
|
|
94
115
|
when "\u007f", "\b" then update_query(@query[0...-1])
|
|
95
116
|
else
|
|
@@ -118,11 +139,20 @@ module Rufio
|
|
|
118
139
|
first = [@selected_index - capacity + 1, 0].max
|
|
119
140
|
@results.slice(first, capacity).to_a.each_with_index do |entry, index|
|
|
120
141
|
selected = first + index == @selected_index
|
|
121
|
-
|
|
122
|
-
text
|
|
142
|
+
prefix = "#{selected ? '>' : ' '} [#{entry[:bookmark]}] "
|
|
143
|
+
text = "#{prefix}#{entry[:relative_path]}"
|
|
144
|
+
text += if entry[:location]
|
|
145
|
+
" - #{entry[:location]}"
|
|
146
|
+
elsif entry[:line_number]
|
|
147
|
+
":#{entry[:line_number]}"
|
|
148
|
+
else
|
|
149
|
+
''
|
|
150
|
+
end
|
|
123
151
|
y = index * row_height + 3
|
|
124
|
-
|
|
125
|
-
|
|
152
|
+
# ファイル名検索はパス、本文検索はスニペットの一致箇所をハイライトする
|
|
153
|
+
path_highlight = @search_type == :filename ? prefix.length : nil
|
|
154
|
+
write_line(screen, y, text, width, selected ? "\e[1;36m" : nil, highlight_from: path_highlight)
|
|
155
|
+
write_line(screen, y + 1, " #{entry[:match_text]}", width, highlight_from: 4) if entry[:match_text]
|
|
126
156
|
end
|
|
127
157
|
if @results.empty?
|
|
128
158
|
message = if @bookmark_count.zero?
|
|
@@ -139,7 +169,7 @@ module Rufio
|
|
|
139
169
|
write_line(screen, 3, message, width)
|
|
140
170
|
end
|
|
141
171
|
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)
|
|
172
|
+
write_line(screen, screen.height - 2, 'ESC: Back | Enter: Go to file | Up/Down or Ctrl+J/K: Select | Type: Search', width)
|
|
143
173
|
end
|
|
144
174
|
|
|
145
175
|
private
|
|
@@ -160,9 +190,14 @@ module Rufio
|
|
|
160
190
|
return unless @scanning
|
|
161
191
|
|
|
162
192
|
roots, events, type, query = @roots, @events, @search_type, @query.downcase
|
|
193
|
+
native_cache = self.class.native_search? ? (@native_cache ||= NativeContentSearch::Cache.new) : nil
|
|
163
194
|
@worker = Thread.new do
|
|
164
195
|
begin
|
|
165
|
-
|
|
196
|
+
if native_cache && type == :content && NativeContentSearch.supports_query?(query)
|
|
197
|
+
scan_roots_natively(roots, events, query, native_cache)
|
|
198
|
+
else
|
|
199
|
+
scan_roots(roots, events, type, query)
|
|
200
|
+
end
|
|
166
201
|
rescue StandardError => e
|
|
167
202
|
events << [:error, e.message]
|
|
168
203
|
end
|
|
@@ -230,7 +265,58 @@ module Rufio
|
|
|
230
265
|
end
|
|
231
266
|
end
|
|
232
267
|
|
|
268
|
+
# Zig 実装で走査・検索し、PDF などのドキュメントだけ Ruby 側で抽出する
|
|
269
|
+
def scan_roots_natively(roots, events, query, cache)
|
|
270
|
+
valid_roots = []
|
|
271
|
+
roots.each do |name, root|
|
|
272
|
+
root = File.expand_path(root)
|
|
273
|
+
raise Errno::ENOTDIR, root unless File.directory?(root)
|
|
274
|
+
|
|
275
|
+
valid_roots << [name, root]
|
|
276
|
+
rescue SystemCallError, IOError => e
|
|
277
|
+
events << [:error, "#{name}: #{e.message}"]
|
|
278
|
+
end
|
|
279
|
+
return if valid_roots.empty?
|
|
280
|
+
|
|
281
|
+
engine = NativeContentSearch.new(roots: valid_roots.map(&:last), query: query,
|
|
282
|
+
excluded_extensions: @excluded_extensions,
|
|
283
|
+
document_extensions: DocumentTextExtractor::EXTENSIONS, cache: cache)
|
|
284
|
+
engine.each_batch do |batch|
|
|
285
|
+
entries = []
|
|
286
|
+
batch.each do |kind, *args|
|
|
287
|
+
case kind
|
|
288
|
+
when :match, :document
|
|
289
|
+
name, root = valid_roots[args[0]]
|
|
290
|
+
path = args[1]
|
|
291
|
+
entry = { path: path, bookmark: name, relative_path: path.delete_prefix(root + File::SEPARATOR) }
|
|
292
|
+
if kind == :document
|
|
293
|
+
scan_content(entry, query, events)
|
|
294
|
+
else
|
|
295
|
+
entries << entry.merge(line_number: args[2], match_text: args[3])
|
|
296
|
+
end
|
|
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
|
+
|
|
233
307
|
def scan_content(entry, query, events)
|
|
308
|
+
if @document_extractor.supported?(entry[:path])
|
|
309
|
+
@document_cache.each_record(entry[:path]) do |record|
|
|
310
|
+
match_index = record[:search_text].index(query)
|
|
311
|
+
next unless match_index
|
|
312
|
+
|
|
313
|
+
snippet = record[:text][[match_index - 30, 0].max, 200].to_s.strip
|
|
314
|
+
metadata = record.reject { |key, _| key == :text || key == :search_text }
|
|
315
|
+
events << [:entry, entry.merge(metadata).merge(match_text: snippet)]
|
|
316
|
+
end
|
|
317
|
+
return
|
|
318
|
+
end
|
|
319
|
+
|
|
234
320
|
File.open(entry[:path], 'rb') do |file|
|
|
235
321
|
sample = file.read(8192).to_s
|
|
236
322
|
if sample.include?("\0")
|
|
@@ -249,13 +335,39 @@ module Rufio
|
|
|
249
335
|
events << [:entry, entry.merge(line_number: number, match_text: snippet)]
|
|
250
336
|
end
|
|
251
337
|
end
|
|
252
|
-
rescue SystemCallError, IOError => e
|
|
338
|
+
rescue SystemCallError, IOError, DocumentTextExtractor::ExtractionError => e
|
|
253
339
|
events << [:error, "#{entry[:path]}: #{e.message}"]
|
|
254
340
|
end
|
|
255
341
|
|
|
256
|
-
def write_line(screen, y, text, width, color = nil)
|
|
342
|
+
def write_line(screen, y, text, width, color = nil, highlight_from: nil)
|
|
257
343
|
safe_text = text.scrub.gsub(/[[:cntrl:]]/, ' ')
|
|
258
|
-
|
|
344
|
+
visible = TextUtils.truncate_to_width(safe_text, width)
|
|
345
|
+
screen.put_string(0, y, visible, fg: color)
|
|
346
|
+
return unless highlight_from
|
|
347
|
+
|
|
348
|
+
match_ranges(visible, highlight_from).each do |start, length|
|
|
349
|
+
x = TextUtils.display_width(visible[0, start])
|
|
350
|
+
screen.put_string(x, y, visible[start, length], fg: MATCH_FG, bg: MATCH_BG)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# 検索と同じく大文字小文字を区別せず、from 文字目以降の一致位置を [開始, 長さ] で返す
|
|
355
|
+
def match_ranges(text, from)
|
|
356
|
+
needle = @query.to_s.downcase.chars
|
|
357
|
+
return [] if needle.empty?
|
|
358
|
+
|
|
359
|
+
folded = text.chars.map(&:downcase)
|
|
360
|
+
ranges = []
|
|
361
|
+
index = from
|
|
362
|
+
while index + needle.length <= folded.length
|
|
363
|
+
if folded[index, needle.length] == needle
|
|
364
|
+
ranges << [index, needle.length]
|
|
365
|
+
index += needle.length
|
|
366
|
+
else
|
|
367
|
+
index += 1
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
ranges
|
|
259
371
|
end
|
|
260
372
|
end
|
|
261
373
|
end
|
data/lib/rufio/help_menu.rb
CHANGED
|
@@ -54,26 +54,31 @@ module Rufio
|
|
|
54
54
|
'Navigation',
|
|
55
55
|
"#{key(:move_down)}/#{key(:move_up)} or arrows: Move down/up",
|
|
56
56
|
"#{key(:navigate_parent)}/#{key(:navigate_enter)}: Parent / enter directory",
|
|
57
|
-
|
|
57
|
+
"#{key(:preview)}: Fullscreen file preview | ESC: Back to file list",
|
|
58
58
|
'Preview: j/k scroll, Ctrl+D/U page down/up',
|
|
59
59
|
"#{key(:top)}/#{key(:bottom)}: First / last entry",
|
|
60
60
|
"#{key(:open_file)}: Open in external editor",
|
|
61
|
+
"#{key(:open_explorer)}: Open directory in file explorer",
|
|
62
|
+
'Preview .zip: Entry list (size, method, date, name)',
|
|
61
63
|
"#{key(:refresh)}: Refresh directory",
|
|
62
64
|
'', 'Selection and file operations',
|
|
63
65
|
"#{key(:select)}: Select / unselect file",
|
|
64
66
|
"#{key(:copy_selected)}/#{key(:move_selected)}/#{key(:delete_selected)}: Copy / move / delete selected files",
|
|
67
|
+
"#{key(:zoxide)}: Extract selected .zip files / compress selected items",
|
|
68
|
+
" Runs as a job: #{key(:job_mode)} Jobs mode shows progress; x cancels",
|
|
65
69
|
"#{key(:rename)}/#{key(:delete)}: Rename / delete current file",
|
|
66
70
|
"#{key(:create_file)}/#{key(:create_dir)}: Create file / directory",
|
|
67
71
|
'', 'Search and bookmarks',
|
|
68
72
|
"#{key(:filter)}: Filter | Enter: Apply | ESC: Clear",
|
|
69
|
-
"#{key(:fzf_search)}: Filename search (fzf)",
|
|
73
|
+
"#{key(:fzf_search)} or #{key(:fzf_search_alt)}: Filename search (fzf)",
|
|
70
74
|
"#{key(:rga_search)}: Content search (rga)",
|
|
71
75
|
"#{key(:global_search)}: Search file names or contents across all bookmarks",
|
|
72
76
|
"#{key(:add_bookmark)}/#{key(:bookmark_menu)}: Add / manage bookmarks",
|
|
73
77
|
'Tab / Shift+Tab: Next / previous bookmark',
|
|
74
78
|
"1-9: Jump to bookmark | #{key(:start_dir)}: Start directory",
|
|
75
|
-
"#{key(:zoxide)}: Directory history (zoxide)",
|
|
76
|
-
"#{key(:quit)}: Quit (with confirmation)"
|
|
79
|
+
"#{key(:zoxide)}: Directory history (zoxide, when nothing is selected)",
|
|
80
|
+
"#{key(:quit)}: Quit (with confirmation)",
|
|
81
|
+
"#{key(:quit_cd)}: Quit and cd to the current directory (rufio --shell-init)"
|
|
77
82
|
]
|
|
78
83
|
when 'Modes'
|
|
79
84
|
[
|
|
@@ -87,20 +92,25 @@ module Rufio
|
|
|
87
92
|
'Search file names or text inside every bookmarked directory.',
|
|
88
93
|
'Tab: Switch File names / Content (literal, case insensitive)',
|
|
89
94
|
'Content results show file, line number and matching text.',
|
|
95
|
+
'Documents: .pdf (page), .xlsx (sheet/cell), .pptx (slide)',
|
|
96
|
+
'Ruby extracts document text; no Python or Office is needed.',
|
|
97
|
+
'Unchanged document text is cached in memory for faster searches.',
|
|
98
|
+
'Images need OCR; legacy .xls/.ppt and slide notes are unsupported.',
|
|
90
99
|
'Yellow Searching... + spinner: Search is running.',
|
|
91
100
|
'Green Search complete: All bookmarked directories scanned.',
|
|
92
101
|
'Exclude extensions in ~/.config/rufio/config.rb:',
|
|
93
102
|
'GLOBAL_SEARCH_EXCLUDED_EXTENSIONS = %w[.zip .log]',
|
|
94
103
|
'Applies to names and contents. Restart Rufio after editing.',
|
|
95
104
|
'Extensions ignore case; leading dots are optional; [] excludes none.',
|
|
96
|
-
'Type to filter | Up/Down or Ctrl+N/P: Select',
|
|
105
|
+
'Type to filter | Up/Down or Ctrl+J/K or Ctrl+N/P: Select',
|
|
97
106
|
'Enter: Go to the selected file in Files mode',
|
|
98
107
|
'ESC: Return to the previous mode | Ctrl+U: Clear query',
|
|
99
108
|
'', "[#{key(:log_viewer)}] Logs",
|
|
100
|
-
|
|
109
|
+
"Browse command logs. #{key(:preview)} opens a fullscreen preview.",
|
|
101
110
|
'ESC: Leave preview; ESC again: Return to Files.',
|
|
102
111
|
'', "[#{key(:job_mode)}] Jobs",
|
|
103
112
|
'View background commands and their status.',
|
|
113
|
+
'ZIP extract/compress jobs show progress here.',
|
|
104
114
|
'Space: View log | x: Cancel job | ESC: Back',
|
|
105
115
|
'', "[#{key(:help)}] Help",
|
|
106
116
|
'This floating menu keeps your current mode and directory.',
|
|
@@ -109,6 +119,7 @@ module Rufio
|
|
|
109
119
|
'Type a command, then Enter to run it.',
|
|
110
120
|
'Tab: Complete | Up/Down: History | ESC: Cancel',
|
|
111
121
|
':!git status: Run a shell command',
|
|
122
|
+
':cd <path>: Go to a pasted path (file: select it, cd -: back)',
|
|
112
123
|
':@hello.rb: Run a script in the current directory'
|
|
113
124
|
]
|
|
114
125
|
when 'Scripts'
|
|
@@ -155,7 +166,8 @@ module Rufio
|
|
|
155
166
|
DialogRenderer.new.draw_floating_window_to_overlay(
|
|
156
167
|
screen, (screen.width - width) / 2, (screen.height - height) / 2,
|
|
157
168
|
width, height, TextUtils.truncate_to_width(title, width - 2), visible,
|
|
158
|
-
border_color: "\e[36m", title_color: "\e[1;36m"
|
|
169
|
+
border_color: "\e[36m", title_color: "\e[1;36m",
|
|
170
|
+
scroll_offset: @scroll_offset, total_lines: lines.length, visible_lines: @page_size
|
|
159
171
|
)
|
|
160
172
|
end
|
|
161
173
|
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'script_file'
|
|
4
|
+
|
|
3
5
|
module Rufio
|
|
4
6
|
# 拡張子からインタープリタを解決するクラス
|
|
5
7
|
class InterpreterResolver
|
|
6
8
|
# デフォルトの拡張子とインタープリタのマッピング
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
".
|
|
10
|
-
|
|
11
|
-
".js" => "node",
|
|
12
|
-
".pl" => "perl",
|
|
13
|
-
".lua" => "lua",
|
|
14
|
-
".ts" => "ts-node",
|
|
15
|
-
".php" => "php",
|
|
16
|
-
".ps1" => nil # プラットフォーム依存
|
|
17
|
-
}.freeze
|
|
9
|
+
# DSLでは .sh にインタープリタを指定し、WindowsのPowerShellを別途解決する。
|
|
10
|
+
DEFAULT_EXTENSIONS = ScriptFile::INTERPRETERS.merge(
|
|
11
|
+
".sh" => "bash", ".lua" => "lua", ".php" => "php", ".ps1" => nil
|
|
12
|
+
).freeze
|
|
18
13
|
|
|
19
14
|
class << self
|
|
20
15
|
# 拡張子からインタープリタを解決する
|
data/lib/rufio/job_manager.rb
CHANGED
|
@@ -23,12 +23,9 @@ module Rufio
|
|
|
23
23
|
job = TaskStatus.new(
|
|
24
24
|
id: @next_id,
|
|
25
25
|
name: name,
|
|
26
|
-
path: path
|
|
26
|
+
path: path,
|
|
27
|
+
command: command
|
|
27
28
|
)
|
|
28
|
-
# commandを保存(TaskStatusに追加)
|
|
29
|
-
job.instance_variable_set(:@command, command)
|
|
30
|
-
job.define_singleton_method(:command) { @command }
|
|
31
|
-
|
|
32
29
|
@jobs << job
|
|
33
30
|
@next_id += 1
|
|
34
31
|
job
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
module Rufio
|
|
6
|
+
# ダイアログなどのブロッキングなキー入力の読み取り口。
|
|
7
|
+
# Windows ではメインループ用の入力スレッドが常に STDIN.getch で待機しているため、
|
|
8
|
+
# ダイアログが STDIN.getch を直接呼ぶと 1 キーずつ入力を奪い合い、2 回押さないと入力できない。
|
|
9
|
+
# 入力キューが接続されている間はキューから読み、それ以外は STDIN.getch を使う。
|
|
10
|
+
module KeyInput
|
|
11
|
+
# Ruby 3.2+ は Queue#pop(timeout:) で正確に待機する(sleep ポーリングは
|
|
12
|
+
# Windows のタイマー分解能 約15ms により遅延する)。それ以前はポーリング。
|
|
13
|
+
QUEUE_POP_TIMEOUT_SUPPORTED = Queue.instance_method(:pop).parameters.any? { |_, n| n == :timeout }
|
|
14
|
+
MULTIBYTE_TIMEOUT = 0.005
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
attr_reader :queue
|
|
18
|
+
|
|
19
|
+
def attach(queue)
|
|
20
|
+
@queue = queue
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def detach
|
|
24
|
+
@queue = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# 1 文字(マルチバイト対応)を読むまでブロックする
|
|
28
|
+
def getch
|
|
29
|
+
queue = @queue
|
|
30
|
+
return STDIN.getch unless queue
|
|
31
|
+
|
|
32
|
+
loop do
|
|
33
|
+
char = build_char(queue.pop, queue)
|
|
34
|
+
return char if char
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# キューからタイムアウト付きで Integer バイトを取り出す
|
|
39
|
+
def pop_with_timeout(queue, timeout_sec)
|
|
40
|
+
return queue.pop(timeout: timeout_sec) if QUEUE_POP_TIMEOUT_SUPPORTED
|
|
41
|
+
|
|
42
|
+
deadline = Time.now + timeout_sec
|
|
43
|
+
loop do
|
|
44
|
+
begin
|
|
45
|
+
return queue.pop(true)
|
|
46
|
+
rescue ThreadError
|
|
47
|
+
return nil if Time.now >= deadline
|
|
48
|
+
|
|
49
|
+
sleep 0.001
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# first_byte_int に続く UTF-8 の後続バイトをキューから読み、1 文字に組み立てる
|
|
55
|
+
def build_char(first_byte_int, queue)
|
|
56
|
+
remaining = case first_byte_int
|
|
57
|
+
when 0x00..0x7F then 0
|
|
58
|
+
when 0xC0..0xDF then 1
|
|
59
|
+
when 0xE0..0xEF then 2
|
|
60
|
+
when 0xF0..0xF7 then 3
|
|
61
|
+
else 0
|
|
62
|
+
end
|
|
63
|
+
return first_byte_int.chr(Encoding::UTF_8) if remaining == 0
|
|
64
|
+
|
|
65
|
+
buf = first_byte_int.chr(Encoding::BINARY)
|
|
66
|
+
remaining.times do
|
|
67
|
+
b = pop_with_timeout(queue, MULTIBYTE_TIMEOUT)
|
|
68
|
+
return nil if b.nil?
|
|
69
|
+
|
|
70
|
+
buf << b.chr(Encoding::BINARY)
|
|
71
|
+
end
|
|
72
|
+
result = buf.force_encoding(Encoding::UTF_8)
|
|
73
|
+
result.valid_encoding? ? result : nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|