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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +45 -21
- data/README_ja.md +45 -23
- data/SECURITY.md +95 -0
- 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 +174 -0
- data/lib/rufio/keybind_handler.rb +53 -1
- data/lib/rufio/tab_mode_manager.rb +5 -1
- data/lib/rufio/terminal_ui.rb +98 -65
- data/lib/rufio/ui_renderer.rb +10 -2
- data/lib/rufio/version.rb +1 -1
- metadata +5 -2
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,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
|
|
6
|
-
|
|
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
|
-
##
|
|
11
|
+
## What Makes rufio Different
|
|
11
12
|
|
|
12
|
-
rufio
|
|
13
|
+
rufio brings finding files, running scripts, and checking results together in one interface.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
###
|
|
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/
|
|
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
|
-
|
|
6
|
-
Ruby
|
|
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
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
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/
|
|
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/
|
|
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
|
|
@@ -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
|
|
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: '?'
|
data/lib/rufio/terminal_ui.rb
CHANGED
|
@@ -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
|
|
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'
|
|
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
|
-
|
|
1196
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
if
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1269
|
+
# ヘルプは非同期オーバーレイとして表示し、元のモードを保持する。
|
|
1270
|
+
def show_help_dialog
|
|
1271
|
+
@help_menu.open
|
|
1272
|
+
true
|
|
1273
|
+
end
|
|
1237
1274
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1275
|
+
def help_dialog_active?
|
|
1276
|
+
@help_menu.active?
|
|
1277
|
+
end
|
|
1240
1278
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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...)の残りバイトを終端アルファベットまで読み捨てる。
|
data/lib/rufio/ui_renderer.rb
CHANGED
|
@@ -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}
|
|
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.
|
|
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
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
|
|
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-
|
|
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
|