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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +33 -1
- data/README_ja.md +33 -1
- data/examples/config.rb +8 -0
- data/lib/rufio/config.rb +2 -2
- data/lib/rufio/config_loader.rb +18 -2
- data/lib/rufio/global_search.rb +261 -0
- data/lib/rufio/help_menu.rb +19 -0
- data/lib/rufio/keybind_handler.rb +42 -0
- data/lib/rufio/tab_mode_manager.rb +5 -1
- data/lib/rufio/terminal_ui.rb +70 -8
- data/lib/rufio/ui_renderer.rb +5 -1
- data/lib/rufio/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c22a65655be19fc13d4e565ea8f926186fc58c7acbff0e16daed2a0efd3362b1
|
|
4
|
+
data.tar.gz: 654b40fe283c94abc1c438b77a2a8eeabc5e5a6321acd01e056d6ccaa5af874b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
data/lib/rufio/config_loader.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Rufio
|
|
|
47
47
|
navigate_parent: 'h',
|
|
48
48
|
navigate_enter: 'l',
|
|
49
49
|
top: 'g',
|
|
50
|
-
bottom: '
|
|
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
|
data/lib/rufio/help_menu.rb
CHANGED
|
@@ -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: '?'
|
data/lib/rufio/terminal_ui.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/rufio/ui_renderer.rb
CHANGED
|
@@ -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.
|
|
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
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
|
+
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
|