rufio 1.0.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 247130c9fd76d05ed1ca70b859ea587e0b42f82ca7491fdc8b1ebea80d0257d4
4
- data.tar.gz: 0122c547241cbd60c870798324b9cddaef6a3767a3153f52ca3b6b6cee3efa07
3
+ metadata.gz: c22a65655be19fc13d4e565ea8f926186fc58c7acbff0e16daed2a0efd3362b1
4
+ data.tar.gz: 654b40fe283c94abc1c438b77a2a8eeabc5e5a6321acd01e056d6ccaa5af874b
5
5
  SHA512:
6
- metadata.gz: 6ef019b2c59bc7bea03293c837ca7ff1f40cb4015e585056c9d12628dd552e33533a6ab3e6d012473110ebac0456978d1bb5c762b6192f13d7d8a269fd0826a9
7
- data.tar.gz: 75138dbdeb740c137045ef17ba6ede44a8a4b2aa85450618e457b3e8d04ca0d5d6157622aadccb9abb934e4c60b4c5a830923406d9854d02128c5f65e4c19e53
6
+ metadata.gz: afb532e1ae55826d8ffeb2607b31717bbae6a2c322493f9a5d81f308bad8c995c714e6638376fbba5ed3878689193917b29814e0ed657d02905a9d8ba8a873c5
7
+ data.tar.gz: 97e60e1b5e180eccaf10fb2c5e501f52ec51dde50fc32f0fa5c5f11c2cbf89b367f970c693f5e2d533efea928d1ccd3a8a9e9aed72c4a1830e0975fb9033a980
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ 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
+
20
+ ## [1.0.4] - 2026-10-25
21
+
22
+ ### Changed
23
+ - Replace directory-based Help with a floating menu containing Controls, Modes, and Scripts sections. Switch sections with 1/2/3 or Tab, scroll with j/k, and close with ESC while preserving the current mode, directory, and preview focus
24
+ - Show mode shortcuts as `[L] Logs`, `[J] Jobs`, and `[?] Help` in the bottom bar
25
+
26
+ ### Fixed
27
+ - Show `ESC: Back to files` below the file name in Preview mode
28
+ - Preserve the original return directory when entering Logs repeatedly and prevent overlapping Help and Logs states
29
+ - Keep mouse interactions inside the floating Help window from affecting the underlying file list
30
+
8
31
  ## [1.0.3] - 2026-10-23
9
32
 
10
33
  ### Security
data/README.md CHANGED
@@ -2,34 +2,26 @@
2
2
 
3
3
  **Runtime Unified Flow I/O Operator**
4
4
 
5
- A TUI file manager that executes and coordinates tools and scripts from files.
6
- Supports Ruby/Python/PowerShell and integrates your development workflow in one place.
5
+ A terminal file manager that keeps your everyday script workflow close at hand.
6
+ Find files, run Ruby, Python, or PowerShell scripts in the current directory, and check jobs and logs.
7
+ Add your own actions in Ruby.
7
8
 
8
9
  [日本語](./README_ja.md) | **English**
9
10
 
10
- ## Concept
11
+ ## What Makes rufio Different
11
12
 
12
- rufio is not just a file manager. It's a **tool runtime execution environment**.
13
+ rufio brings finding files, running scripts, and checking results together in one interface.
13
14
 
14
- ```
15
- ┌─────────────────────────────────────────────────────────┐
16
- │ rufio │
17
- │ Runtime Unified Flow I/O Operator │
18
- ├─────────────────────────────────────────────────────────┤
19
- │ Files ──→ Scripts ──→ Tools ──→ Output │
20
- │ ↑ │ │
21
- │ └───────────── Feedback ────────────┘ │
22
- └─────────────────────────────────────────────────────────┘
23
- ```
24
-
25
- - **File Operations**: Traditional file manager functionality
26
- - **Script Execution**: Run Ruby/Python/PowerShell scripts in file context
27
- - **Tool Integration**: Seamless integration with external tools (git, fzf, rga, etc.)
28
- - **Unified I/O**: Manage all input/output in a single flow
15
+ - **Run scripts where your files are**: Register your script directories, then press `:`, type `@` + part of a script name, press `Tab`, and hit `Enter`. Run Ruby, Python, or PowerShell scripts in the directory you are browsing
16
+ - **Follow jobs and execution logs**: Keep browsing while scripts run in the background. Press `J` to check jobs and `L` to view execution logs
17
+ - **Add your own actions in Ruby**: Define custom commands with a small Ruby DSL in `~/.config/rufio/commands.rb`. `~/.rufio/commands.rb` is also supported
18
+ - **Use installed tools without extra configuration**: Search file names with fzf, search contents with rga, highlight previews with bat, and navigate directory history with zoxide
19
+ - **Windows support**: Run PowerShell scripts with keyboard input handling adapted to Windows terminals
20
+ - **Trace behavior in the source**: The implementation is primarily Ruby, so you can inspect and change the parts you need. Security design is documented in [SECURITY.md](./SECURITY.md)
29
21
 
30
22
  ## Features
31
23
 
32
- ### As a Tool Runtime
24
+ ### For Script Workflows
33
25
 
34
26
  - **Multi-language Script Support**: Ruby, Python, PowerShell
35
27
  - **Script Path Management**: Register and manage multiple script directories
@@ -92,7 +84,7 @@ rufio /path/to # Launch in specified directory
92
84
  |-----|----------|
93
85
  | `j/k` | Move up/down |
94
86
  | `h/l` | Parent/child directory |
95
- | `g/G` | Top/bottom |
87
+ | `g/}` | Top/bottom |
96
88
  | `Enter` | Enter directory/open file |
97
89
  | `q` | Quit |
98
90
 
@@ -131,11 +123,43 @@ rufio /path/to # Launch in specified directory
131
123
  | Key | Function |
132
124
  |-----|----------|
133
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) |
134
128
  | `J` | Job mode |
135
129
  | `L` | View execution logs |
136
130
  | `?` | Help |
137
131
  | `Shift+Tab` | Switch mode (reverse) |
138
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
+
139
163
  ## Command Mode
140
164
 
141
165
  Press `:` to enter command mode and execute various commands.
data/README_ja.md CHANGED
@@ -2,36 +2,26 @@
2
2
 
3
3
  **Runtime Unified Flow I/O Operator**
4
4
 
5
- ファイルを起点に、ツールとスクリプトを実行・連携させるTUIファイルマネージャー。
6
- Ruby/Python/PowerShellに対応し、開発ワークフローを一箇所に統合します。
7
-
8
- A TUI file manager as a unified runtime environment for tools and scripts.
5
+ 日々のスクリプト作業を手元にまとめるターミナルファイラ。
6
+ ファイルを探し、その場所で Ruby・Python・PowerShell を実行し、ジョブとログを確認できます。
7
+ 自分用の操作は Ruby で追加できます。
9
8
 
10
9
  **日本語** | [English](./README.md)
11
10
 
12
- ## コンセプト
13
-
14
- rufioは単なるファイルマネージャーではありません。**ツールランタイム実行環境**です。
11
+ ## rufioならではの点
15
12
 
16
- ```
17
- ┌─────────────────────────────────────────────────────────┐
18
- │ rufio │
19
- │ Runtime Unified Flow I/O Operator │
20
- ├─────────────────────────────────────────────────────────┤
21
- │ Files ──→ Scripts ──→ Tools ──→ Output │
22
- │ ↑ │ │
23
- │ └───────────── Feedback ────────────┘ │
24
- └─────────────────────────────────────────────────────────┘
25
- ```
13
+ rufio は、ファイルを探すところからスクリプトの実行、結果の確認までを一つの画面にまとめます。
26
14
 
27
- - **ファイル操作**: 従来のファイルマネージャー機能
28
- - **スクリプト実行**: Ruby/Python/PowerShellスクリプトをファイルコンテキストで実行
29
- - **ツール連携**: 外部ツール(git, fzf, rga等)とのシームレスな統合
30
- - **統一I/O**: すべての入出力を単一のフローで管理
15
+ - **ファイルのある場所からスクリプトを実行**: スクリプトのディレクトリを登録し、`:` → `@` + スクリプト名の一部 → `Tab` → `Enter` で呼び出せます。Ruby・Python・PowerShell を、いま開いているディレクトリで実行できます
16
+ - **ジョブとログで実行結果を追える**: バックグラウンドで実行しながらファイル操作を続け、`J` でジョブ、`L` で実行ログを確認できます
17
+ - **自分用の操作を Ruby で追加**: `~/.config/rufio/commands.rb` に小さな Ruby DSL で独自コマンドを定義できます。`~/.rufio/commands.rb` にも対応しています
18
+ - **インストール済みのツールと追加設定なしで連携**: fzf でファイル名検索、rga で内容検索、bat でプレビューのシンタックスハイライト、zoxide でディレクトリ履歴から移動できます
19
+ - **Windows にも対応**: PowerShell スクリプトの実行に対応し、Windows のターミナルに合わせたキー入力処理を備えています
20
+ - **挙動をソースで追いやすい**: Ruby を中心とした実装で、必要な処理を自分で確認・修正できます。セキュリティに関する設計は [SECURITY.md](./SECURITY.md) にまとめています
31
21
 
32
22
  ## 特徴
33
23
 
34
- ### ツールランタイムとして
24
+ ### スクリプト作業のために
35
25
 
36
26
  - **マルチ言語スクリプト対応**: Ruby, Python, PowerShell
37
27
  - **スクリプトパス管理**: 複数のスクリプトディレクトリを登録・管理
@@ -94,7 +84,7 @@ rufio /path/to # 指定したディレクトリで起動
94
84
  |------|------|
95
85
  | `j/k` | 上下移動 |
96
86
  | `h/l` | 親/子ディレクトリ |
97
- | `g/G` | 先頭/末尾 |
87
+ | `g/}` | 先頭/末尾 |
98
88
  | `Enter` | ディレクトリに入る/ファイルを開く |
99
89
  | `q` | 終了 |
100
90
 
@@ -136,8 +126,40 @@ rufio /path/to # 指定したディレクトリで起動
136
126
  | `J` | ジョブモード |
137
127
  | `L` | 実行ログ表示 |
138
128
  | `?` | ヘルプ |
129
+ | `G` | 全ブックマーク配下のファイル名・ファイル内容を横断検索 |
130
+ | `C` | ~/.config/rufio/を開く(Configモード、操作はFilesと共通) |
139
131
  | `Shift+Tab` | モード切り替え(逆順) |
140
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
+
141
163
  ## コマンドモード
142
164
 
143
165
  `:` でコマンドモードを起動し、様々なコマンドを実行できます。
data/SECURITY.md ADDED
@@ -0,0 +1,95 @@
1
+ # Security Policy / セキュリティポリシー
2
+
3
+ [English](#english) | [日本語](#日本語)
4
+
5
+ ## English
6
+
7
+ ### Supported versions
8
+
9
+ Only the latest released version receives security fixes. Please upgrade with `gem update rufio` before reporting.
10
+
11
+ ### Reporting a vulnerability
12
+
13
+ Please **do not open a public issue** for security problems.
14
+
15
+ - Use GitHub's private reporting: [Security → Report a vulnerability](https://github.com/masisz/rufio/security/advisories/new)
16
+ - Or email masisz.1567@gmail.com
17
+
18
+ Include the rufio version, OS, steps to reproduce, and the impact you expect. This is a personal project, so responses are best-effort, but reports are taken seriously and fixes are noted in [CHANGELOG.md](./CHANGELOG.md) under `Security`.
19
+
20
+ ### Threat model
21
+
22
+ rufio is a local TUI tool run by you, on files you can already access.
23
+
24
+ **In scope** (treated as vulnerabilities):
25
+ - Names or contents of files, directories, archives, or tool output causing unintended command execution (e.g. shell metacharacters in file names)
26
+ - Escape sequences in file contents or names that manipulate the terminal in harmful ways
27
+ - Unsafe handling of paths that lets an operation affect files outside what the user selected
28
+ - Secrets exposed through logs or temporary files
29
+
30
+ **Out of scope** (by design):
31
+ - Commands you type yourself in command mode (`:`, `!`) — they are executed as shell commands
32
+ - Code in your own configuration: `~/.rufio/commands.rb` and `~/.config/rufio/commands.rb` are **evaluated as Ruby**. Keep them writable only by you
33
+ - Scripts you register under script paths — they run with your privileges
34
+ - Vulnerabilities in external tools (fzf, rga, bat, zoxide, editors)
35
+
36
+ ### Design practices
37
+
38
+ - Files and scripts are launched in array form (`system(*argv)`), never through a shell string; paths beginning with `-` are prefixed with `./`
39
+ - Search queries are passed to rga after `--`
40
+ - YAML is loaded with `YAML.safe_load`
41
+ - Command execution logs are written with `0600` permissions
42
+ - Runtime dependencies are limited to `io-console`, `pastel`, `tty-cursor`, `tty-screen`
43
+ - Gems are published from tagged commits via RubyGems Trusted Publishing (no long-lived API key); CI runs the test suite before publishing
44
+
45
+ ### Known limitations
46
+
47
+ - Text previews only detect binary files (by control-character ratio); terminal escape sequences inside otherwise-textual files are not explicitly stripped. Avoid previewing untrusted files in a terminal you do not trust
48
+ - The optional native extension under `lib_zig` is not shipped in the gem and has not been independently audited
49
+
50
+ ---
51
+
52
+ ## 日本語
53
+
54
+ ### サポート対象バージョン
55
+
56
+ セキュリティ修正は最新のリリースにのみ行います。報告の前に `gem update rufio` で更新してください。
57
+
58
+ ### 脆弱性の報告
59
+
60
+ セキュリティ上の問題は**公開Issueに書かない**でください。
61
+
62
+ - GitHubの非公開報告: [Security → Report a vulnerability](https://github.com/masisz/rufio/security/advisories/new)
63
+ - またはメール: masisz.1567@gmail.com
64
+
65
+ rufioのバージョン、OS、再現手順、想定される影響を添えてください。個人プロジェクトのため対応はベストエフォートですが、報告は真剣に扱い、修正は [CHANGELOG.md](./CHANGELOG.md) の `Security` に記載します。
66
+
67
+ ### 脅威モデル
68
+
69
+ rufioは、あなた自身がアクセスできるファイルに対して、ローカルで実行するTUIツールです。
70
+
71
+ **対象(脆弱性として扱う)**:
72
+ - ファイル名・ディレクトリ名・アーカイブ・ツール出力の内容によって、意図しないコマンドが実行されること(例: ファイル名中のシェルメタ文字)
73
+ - ファイル内容や名前に含まれるエスケープシーケンスによる、有害なターミナル操作
74
+ - パスの扱いの不備により、ユーザーが選択した範囲外のファイルに操作が及ぶこと
75
+ - ログや一時ファイルを通じた機密情報の漏えい
76
+
77
+ **対象外(設計上の仕様)**:
78
+ - コマンドモード(`:`、`!`)で自分で入力したコマンド — シェルコマンドとして実行されます
79
+ - 自分の設定ファイル内のコード — `~/.rufio/commands.rb` と `~/.config/rufio/commands.rb` は**Rubyとして評価**されます。書き込み権限は自分だけにしてください
80
+ - スクリプトパスに登録したスクリプト — あなたの権限で実行されます
81
+ - 外部ツール(fzf、rga、bat、zoxide、エディタ)自体の脆弱性
82
+
83
+ ### 設計上の方針
84
+
85
+ - ファイルやスクリプトは配列形式(`system(*argv)`)で起動し、シェル文字列を介しません。`-` で始まるパスには `./` を付与します
86
+ - rga には `--` を挟んで検索語を渡します
87
+ - YAML は `YAML.safe_load` で読み込みます
88
+ - コマンド実行ログは `0600` で保存します
89
+ - 実行時依存gemは `io-console`、`pastel`、`tty-cursor`、`tty-screen` のみです
90
+ - gemはタグ付きコミットからRubyGemsのTrusted Publishingで公開します(長期APIキーなし)。公開前にCIでテストを実行します
91
+
92
+ ### 既知の制約
93
+
94
+ - テキストのプレビューはバイナリ判定(制御文字の比率)のみで、テキストとして扱われるファイル内のエスケープシーケンスは明示的に除去していません。信頼できないファイルを、信頼できないターミナルでプレビューするのは避けてください
95
+ - `lib_zig` のネイティブ拡張(任意機能)はgemに同梱しておらず、第三者による監査も受けていません
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/G:top/bottom r:refresh f:filter s:search F:content a/A:create m/p/x:ops b:bookmark z:zoxide 1-9:goto q:quit',
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/G:top/end r:refresh f:filter s:search F:content a/A:create m/c/x:ops b:bookmark z:zoxide 1-9:jump q:quit',
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
@@ -47,7 +47,7 @@ module Rufio
47
47
  navigate_parent: 'h',
48
48
  navigate_enter: 'l',
49
49
  top: 'g',
50
- bottom: 'G',
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
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'text_utils'
4
+ require_relative 'dialog_renderer'
5
+ require_relative 'config_loader'
6
+
7
+ module Rufio
8
+ # メインループを止めずに表示するヘルプメニュー。
9
+ class HelpMenu
10
+ SECTIONS = %w[Controls Modes Scripts].freeze
11
+ attr_reader :scroll_offset
12
+
13
+ def initialize
14
+ @active = false
15
+ @section_index = 0
16
+ @scroll_offset = 0
17
+ @max_scroll = 0
18
+ end
19
+
20
+ def open
21
+ @active = true
22
+ @section_index = 0
23
+ @scroll_offset = 0
24
+ end
25
+
26
+ def active?
27
+ @active
28
+ end
29
+
30
+ def section
31
+ SECTIONS[@section_index]
32
+ end
33
+
34
+ def handle_key(key)
35
+ case key
36
+ when "\e", 'q', '?' then @active = false
37
+ when '1', '2', '3' then select_section(key.to_i - 1)
38
+ when "\t", 'l' then select_section((@section_index + 1) % SECTIONS.length)
39
+ when 'h', "\e[Z" then select_section((@section_index - 1) % SECTIONS.length)
40
+ when 'j' then @scroll_offset = [@scroll_offset + 1, @max_scroll].min
41
+ when 'k' then @scroll_offset = [@scroll_offset - 1, 0].max
42
+ when "\x04" then @scroll_offset = [@scroll_offset + @page_size.to_i, @max_scroll].min
43
+ when "\x15" then @scroll_offset = [@scroll_offset - @page_size.to_i, 0].max
44
+ when 'g' then @scroll_offset = 0
45
+ when 'G' then @scroll_offset = @max_scroll
46
+ end
47
+ true
48
+ end
49
+
50
+ def content
51
+ case section
52
+ when 'Controls'
53
+ [
54
+ 'Navigation',
55
+ "#{key(:move_down)}/#{key(:move_up)} or arrows: Move down/up",
56
+ "#{key(:navigate_parent)}/#{key(:navigate_enter)}: Parent / enter directory",
57
+ 'Enter: Open file preview | ESC: Back to file list',
58
+ 'Preview: j/k scroll, Ctrl+D/U page down/up',
59
+ "#{key(:top)}/#{key(:bottom)}: First / last entry",
60
+ "#{key(:open_file)}: Open in external editor",
61
+ "#{key(:refresh)}: Refresh directory",
62
+ '', 'Selection and file operations',
63
+ "#{key(:select)}: Select / unselect file",
64
+ "#{key(:copy_selected)}/#{key(:move_selected)}/#{key(:delete_selected)}: Copy / move / delete selected files",
65
+ "#{key(:rename)}/#{key(:delete)}: Rename / delete current file",
66
+ "#{key(:create_file)}/#{key(:create_dir)}: Create file / directory",
67
+ '', 'Search and bookmarks',
68
+ "#{key(:filter)}: Filter | Enter: Apply | ESC: Clear",
69
+ "#{key(:fzf_search)}: Filename search (fzf)",
70
+ "#{key(:rga_search)}: Content search (rga)",
71
+ "#{key(:global_search)}: Search file names or contents across all bookmarks",
72
+ "#{key(:add_bookmark)}/#{key(:bookmark_menu)}: Add / manage bookmarks",
73
+ 'Tab / Shift+Tab: Next / previous bookmark',
74
+ "1-9: Jump to bookmark | #{key(:start_dir)}: Start directory",
75
+ "#{key(:zoxide)}: Directory history (zoxide)",
76
+ "#{key(:quit)}: Quit (with confirmation)"
77
+ ]
78
+ when 'Modes'
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',
99
+ '', "[#{key(:log_viewer)}] Logs",
100
+ 'Browse command logs. Enter opens a preview.',
101
+ 'ESC: Leave preview; ESC again: Return to Files.',
102
+ '', "[#{key(:job_mode)}] Jobs",
103
+ 'View background commands and their status.',
104
+ 'Space: View log | x: Cancel job | ESC: Back',
105
+ '', "[#{key(:help)}] Help",
106
+ 'This floating menu keeps your current mode and directory.',
107
+ '1/2/3 or Tab: Select section | ESC: Close',
108
+ '', "[#{key(:command_mode)}] Command",
109
+ 'Type a command, then Enter to run it.',
110
+ 'Tab: Complete | Up/Down: History | ESC: Cancel',
111
+ ':!git status: Run a shell command',
112
+ ':@hello.rb: Run a script in the current directory'
113
+ ]
114
+ when 'Scripts'
115
+ [
116
+ '1. Personal scripts',
117
+ 'Place scripts in ~/.config/rufio/scripts/',
118
+ 'Example: ~/.config/rufio/scripts/hello.rb',
119
+ 'Press : and type hello, then Enter (:hello).',
120
+ '', '2. Additional script folders',
121
+ 'List folders in ~/.config/rufio/script_paths.yml:',
122
+ '- ~/.config/rufio/scripts', '- ~/my-scripts',
123
+ 'Folders are searched in order. First matching name wins.',
124
+ '', '3. Project scripts',
125
+ 'Place hello.rb or hello.sh in the directory you browse.',
126
+ 'Run it with :@hello.rb or :@hello.sh.',
127
+ 'Project search paths: ./rufio.yml, script_paths list.',
128
+ '', '4. Run and monitor',
129
+ 'Tab completes script names. Up/Down recalls commands.',
130
+ "#{key(:job_mode)}: Background jobs | #{key(:log_viewer)}: Execution logs",
131
+ 'Supported: Ruby, shell, Python and other script types.'
132
+ ]
133
+ end
134
+ end
135
+
136
+ def draw(screen)
137
+ width = [screen.width, 78].min
138
+ height = [screen.height, 26].min
139
+ return if width < 8 || height < 8
140
+
141
+ @page_size = height - 7
142
+ lines = content.flat_map { |line| line.empty? ? [''] : TextUtils.wrap_preview_lines([line], width - 4) }
143
+ @max_scroll = [lines.length - @page_size, 0].max
144
+ @scroll_offset = [@scroll_offset, @max_scroll].min
145
+ tabs = SECTIONS.each_with_index.map do |name, index|
146
+ index == @section_index ? "[#{index + 1}:#{name}]" : " #{index + 1}:#{name} "
147
+ end.join(' ')
148
+ visible = [TextUtils.truncate_to_width(tabs, width - 2), '']
149
+ visible.concat(lines.slice(@scroll_offset, @page_size) || [])
150
+ visible << '' while visible.length < height - 5
151
+ visible << TextUtils.truncate_to_width('ESC: Close | Tab: Menu | j/k: Scroll', width - 2)
152
+ screen.enable_overlay
153
+ screen.clear_overlay
154
+ title = "Help - #{section} (#{@scroll_offset + 1}/#{@max_scroll + 1})"
155
+ DialogRenderer.new.draw_floating_window_to_overlay(
156
+ screen, (screen.width - width) / 2, (screen.height - height) / 2,
157
+ width, height, TextUtils.truncate_to_width(title, width - 2), visible,
158
+ border_color: "\e[36m", title_color: "\e[1;36m"
159
+ )
160
+ end
161
+
162
+ private
163
+
164
+ def select_section(index)
165
+ @section_index = index
166
+ @scroll_offset = 0
167
+ end
168
+
169
+ def key(action)
170
+ value = ConfigLoader.keybinds[action]
171
+ value == ' ' ? 'Space' : value.to_s
172
+ end
173
+ end
174
+ end
@@ -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
@@ -130,6 +164,18 @@ module Rufio
130
164
  def handle_key(key)
131
165
  return false unless @directory_listing
132
166
 
167
+ if @key_map[key] == :help && !@filter_manager.filter_mode
168
+ return show_help_dialog
169
+ end
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
+
133
179
  # ジョブモード中の特別処理
134
180
  if @job_mode.active?
135
181
  return handle_job_mode_key(key)
@@ -217,6 +263,9 @@ module Rufio
217
263
  # ヘルプモードに入る
218
264
  def enter_help_mode
219
265
  return false unless @directory_listing
266
+ return true if @in_help_mode
267
+
268
+ exit_log_viewer_mode if @in_log_viewer_mode
220
269
 
221
270
  # 現在のディレクトリを保存
222
271
  @pre_help_directory = @directory_listing.current_path
@@ -264,6 +313,9 @@ module Rufio
264
313
  # ログビューワモードに入る
265
314
  def enter_log_viewer_mode
266
315
  return false unless @directory_listing
316
+ return true if @in_log_viewer_mode
317
+
318
+ exit_help_mode if @in_help_mode
267
319
 
268
320
  # 現在のディレクトリを保存
269
321
  @pre_log_viewer_directory = @directory_listing.current_path
@@ -488,7 +540,7 @@ module Rufio
488
540
  when :zoxide then show_zoxide_menu
489
541
  when :start_dir then goto_start_directory
490
542
  when :job_mode then enter_job_mode
491
- when :help then enter_help_mode
543
+ when :help then show_help_dialog
492
544
  when :log_viewer then enter_log_viewer_mode
493
545
  when :command_mode then activate_command_mode
494
546
  when :quit then exit_request
@@ -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: '?'
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'io/console'
4
4
  require_relative 'text_utils'
5
+ require_relative 'help_menu'
6
+ require_relative 'global_search'
5
7
 
6
8
  module Rufio
7
9
  class TerminalUI
@@ -51,6 +53,8 @@ module Rufio
51
53
  @input_queue = nil
52
54
  @input_thread = nil
53
55
  @command_mode_active = false
56
+ @help_menu = HelpMenu.new
57
+ @global_search = GlobalSearch.new
54
58
  @command_input = ""
55
59
  @command_mode = CommandMode.new
56
60
  @dialog_renderer = DialogRenderer.new
@@ -302,6 +306,7 @@ module Rufio
302
306
  end
303
307
 
304
308
  def cleanup_terminal
309
+ @global_search.close
305
310
  # マウスレポートを先に無効化して新しいイベントの流入を止める
306
311
  if windows?
307
312
  print "\e[?1000l\e[?1006l"
@@ -473,6 +478,8 @@ module Rufio
473
478
  needs_redraw = true
474
479
  end
475
480
 
481
+ needs_redraw = true if @global_search.poll
482
+
476
483
  # 非同期シンタックスハイライト完了チェック(UIRenderer側のフラグを参照)
477
484
  if @ui_renderer.highlight_updated?
478
485
  @ui_renderer.reset_highlight_updated
@@ -494,8 +501,10 @@ module Rufio
494
501
  draw_screen_to_buffer(@screen, nil, current_fps)
495
502
  end
496
503
 
497
- # コマンドモードがアクティブな場合はオーバーレイにダイアログを描画
498
- if @command_mode_active
504
+ # ヘルプまたはコマンドモードのダイアログをオーバーレイに描画
505
+ if help_dialog_active?
506
+ @help_menu.draw(@screen)
507
+ elsif @command_mode_active
499
508
  # 前回のオーバーレイ残留を防ぐためクリアしてから描画
500
509
  @screen.clear_overlay if @screen.overlay_enabled?
501
510
  draw_command_mode_to_overlay
@@ -525,6 +534,12 @@ module Rufio
525
534
 
526
535
  # UIRenderer に全描画処理を委譲
527
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
+
528
543
  @ui_renderer.draw_screen_to_buffer(
529
544
  screen, notification_message, fps,
530
545
  in_job_mode: @in_job_mode,
@@ -594,11 +609,17 @@ module Rufio
594
609
  return true
595
610
  end
596
611
  input = case third_char
597
- when 'A' then 'k' # Up arrow
598
- when 'B' then 'j' # Down arrow
599
- when 'C' then 'l' # Right arrow
600
- when 'D' then 'h' # Left arrow
601
- when 'Z' then handle_shift_tab; return true # Shift+Tab
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
616
+ when 'Z'
617
+ if help_dialog_active?
618
+ handle_help_input("\e[Z")
619
+ elsif !global_search_active?
620
+ handle_shift_tab
621
+ end
622
+ return true # Shift+Tab
602
623
  else
603
624
  # 未知の CSI シーケンス(\e[27;1u 等): 残りバイトを読み捨てて
604
625
  # パイプを汚染しないようにする
@@ -610,14 +631,17 @@ module Rufio
610
631
  end
611
632
  end
612
633
 
634
+ return handle_help_input(input) if help_dialog_active?
635
+ return handle_global_search_input(input) if global_search_active?
636
+
613
637
  # TabキーはFilesモードの時のみブックマーク循環移動(ジョブモード中は無効)
614
- 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
615
639
  handle_tab_key
616
640
  return true
617
641
  end
618
642
 
619
643
  # 数字キー(1-9): Filesモード かつ フィルターモード外でブックマークジャンプ+ハイライト
620
- 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?
621
645
  handle_bookmark_key(input.to_i)
622
646
  return true
623
647
  end
@@ -670,7 +694,7 @@ module Rufio
670
694
 
671
695
  # Shift+Tab: Filesモードでは前のブックマークへ循環移動、それ以外はモード逆順切り替え
672
696
  def handle_shift_tab
673
- if @tab_mode_manager.current_mode == :files
697
+ if @tab_mode_manager.current_mode == :files || @keybind_handler.config_mode?
674
698
  prev_idx = @keybind_handler.goto_prev_bookmark
675
699
  if prev_idx
676
700
  @ui_renderer.set_highlighted_bookmark(prev_idx + 1)
@@ -692,6 +716,18 @@ module Rufio
692
716
  # @param row [Integer] クリック行(1-indexed)
693
717
  # @param press [Boolean] true=押下, false=解放
694
718
  def handle_mouse_event(btn, col, row, press)
719
+ if help_dialog_active?
720
+ handle_help_input('k') if btn == 64
721
+ handle_help_input('j') if btn == 65
722
+ return
723
+ end
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
+
695
731
  case btn
696
732
  when 0 # 左クリック(押下のみ処理)
697
733
  handle_mouse_left_click(col, row) if press
@@ -774,6 +810,10 @@ module Rufio
774
810
  # モード変更を適用
775
811
  def apply_mode_change(mode)
776
812
  case mode
813
+ when :config
814
+ @keybind_handler.enter_config_mode
815
+ when :global
816
+ @keybind_handler.start_global_search
777
817
  when :files
778
818
  # ヘルプモードまたはログビューワモードから戻る
779
819
  if @keybind_handler.help_mode?
@@ -784,12 +824,7 @@ module Rufio
784
824
  @keybind_handler.send(:exit_job_mode)
785
825
  end
786
826
  when :help
787
- # ヘルプモードに入る
788
- unless @keybind_handler.help_mode?
789
- @keybind_handler.send(:exit_log_viewer_mode) if @keybind_handler.log_viewer_mode?
790
- @keybind_handler.send(:exit_job_mode) if @keybind_handler.in_job_mode?
791
- @keybind_handler.send(:enter_help_mode)
792
- end
827
+ show_help_dialog
793
828
  when :logs
794
829
  # ログビューワモードに入る
795
830
  unless @keybind_handler.log_viewer_mode?
@@ -1191,61 +1226,59 @@ module Rufio
1191
1226
  # Screen と Renderer のアクセサ(他のクラスから利用可能に)
1192
1227
  attr_reader :screen, :renderer
1193
1228
 
1194
- # ヘルプダイアログを表示
1195
- def show_help_dialog
1196
- content_lines = [
1197
- '',
1198
- "rufio v#{VERSION}",
1199
- '',
1200
- 'Key Bindings:',
1201
- '',
1202
- 'j/k - Move up/down',
1203
- 'h/l - Navigate back/enter',
1204
- 'g/G - Go to top/bottom',
1205
- 'o - Open file',
1206
- 'f - Filter files',
1207
- 's - Search with fzf',
1208
- 'F - Content search (rga)',
1209
- 'a/A - Create file/directory',
1210
- 'm/c/x - Move/Copy/Delete',
1211
- 'b - Add bookmark',
1212
- 'z - Zoxide navigation',
1213
- '0 - Go to start directory',
1214
- '1-9 - Go to bookmark',
1215
- 'J - Job mode',
1216
- ': - Command mode',
1217
- 'q - Quit',
1218
- ''
1219
- ]
1229
+ def start_global_search(bookmarks)
1230
+ @global_search.open(bookmarks)
1231
+ end
1220
1232
 
1221
- # お知らせ情報を追加
1222
- require_relative 'info_notice'
1223
- info_notice = InfoNotice.new
1224
- all_notices = Dir.glob(File.join(info_notice.info_dir, '*.txt'))
1225
-
1226
- if !all_notices.empty?
1227
- content_lines << 'Recent Updates:'
1228
- content_lines << ''
1229
- all_notices.take(3).each do |file|
1230
- title = info_notice.extract_title(file)
1231
- content_lines << " • #{title}"
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
1232
1252
  end
1233
- content_lines << ''
1234
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
1235
1268
 
1236
- content_lines << 'Press any key to continue...'
1269
+ # ヘルプは非同期オーバーレイとして表示し、元のモードを保持する。
1270
+ def show_help_dialog
1271
+ @help_menu.open
1272
+ true
1273
+ end
1237
1274
 
1238
- width = 60
1239
- height = [content_lines.length + 4, @screen_height - 4].min
1275
+ def help_dialog_active?
1276
+ @help_menu.active?
1277
+ end
1240
1278
 
1241
- # オーバーレイダイアログを表示
1242
- show_overlay_dialog('rufio - Help', content_lines, {
1243
- width: width,
1244
- height: height,
1245
- border_color: "\e[36m", # Cyan
1246
- title_color: "\e[1;36m", # Bold cyan
1247
- content_color: "\e[37m" # White
1248
- })
1279
+ def handle_help_input(input)
1280
+ @help_menu.handle_key(input)
1281
+ true
1249
1282
  end
1250
1283
 
1251
1284
  # 未知の CSI シーケンス(\e[X...)の残りバイトを終端アルファベットまで読み捨てる。
@@ -401,6 +401,10 @@ module Rufio
401
401
  header += ' ' * remaining_space if remaining_space > 0
402
402
  screen.put_string(content_x, line_num, header)
403
403
 
404
+ elsif i == 1 && @keybind_handler&.preview_focused?
405
+ hint = TextUtils.truncate_to_width(' ESC: Back to files', safe_width)
406
+ screen.put_string(content_x, line_num, hint + ' ' * (safe_width - TextUtils.display_width(hint)))
407
+
404
408
  elsif i >= 2 && highlighted_wrapped_lines
405
409
  # シンタックスハイライト付きコンテンツ
406
410
  scroll_offset = @keybind_handler&.preview_scroll_offset || 0
@@ -583,7 +587,7 @@ module Rufio
583
587
 
584
588
  modes.each_with_index do |mode, index|
585
589
  key = keys[mode]
586
- label = key ? " #{key}:#{labels[mode]} " : " #{labels[mode]} "
590
+ label = key ? " [#{key}] #{labels[mode]} " : " #{labels[mode]} "
587
591
 
588
592
  if mode == current_mode
589
593
  label.each_char do |char|
@@ -778,12 +782,16 @@ module Rufio
778
782
  def sync_tab_mode_with_keybind_handler
779
783
  return unless @keybind_handler
780
784
 
781
- current_mode = if @keybind_handler.in_job_mode?
785
+ current_mode = if @keybind_handler.global_search_mode?
786
+ :global
787
+ elsif @keybind_handler.in_job_mode?
782
788
  :jobs
783
789
  elsif @keybind_handler.help_mode?
784
790
  :help
785
791
  elsif @keybind_handler.log_viewer_mode?
786
792
  :logs
793
+ elsif @keybind_handler.config_mode?
794
+ :config
787
795
  else
788
796
  :files
789
797
  end
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '1.0.3'
4
+ VERSION = '1.1.0'
5
5
  end
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.0.3
4
+ version: 1.1.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-10-23 00:00:00.000000000 Z
11
+ date: 2026-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: io-console
@@ -121,6 +121,7 @@ files:
121
121
  - README.md
122
122
  - README_ja.md
123
123
  - Rakefile
124
+ - SECURITY.md
124
125
  - bin/rufio
125
126
  - docs/CHANGELOG_v0.10.0.md
126
127
  - docs/CHANGELOG_v0.20.0.md
@@ -174,7 +175,9 @@ files:
174
175
  - lib/rufio/file_operations.rb
175
176
  - lib/rufio/file_preview.rb
176
177
  - lib/rufio/filter_manager.rb
178
+ - lib/rufio/global_search.rb
177
179
  - lib/rufio/health_checker.rb
180
+ - lib/rufio/help_menu.rb
178
181
  - lib/rufio/info_notice.rb
179
182
  - lib/rufio/interpreter_resolver.rb
180
183
  - lib/rufio/job_manager.rb