rufio 0.71.0 → 0.80.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 +27 -0
- data/README.md +29 -10
- data/README_ja.md +30 -10
- data/docs/CHANGELOG_v0.80.0.md +189 -0
- data/lib/rufio/ansi_line_parser.rb +103 -0
- data/lib/rufio/config.rb +2 -0
- data/lib/rufio/file_preview.rb +26 -1
- data/lib/rufio/health_checker.rb +22 -0
- data/lib/rufio/renderer.rb +11 -4
- data/lib/rufio/screen.rb +3 -2
- data/lib/rufio/syntax_highlighter.rb +111 -0
- data/lib/rufio/terminal_ui.rb +129 -28
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +2 -0
- 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: a5c4fc956ed9bcb90569c8c7fcdd023fec3d31cdf1852f075ba40c9746a654dc
|
|
4
|
+
data.tar.gz: f07ae9f1f0071de307b407e5322f3f2a53c62fb654f023223f7214be79350dc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '097aed068bd564b961c159504b7396c4d0dec7b9eb4be819d2f83380fc10aa86e698c316b9e95bfb012863790ea53cbe0ee521d3561919cdde61fbafcbb22959'
|
|
7
|
+
data.tar.gz: 6d1d2ce8640debc3fe6fbe282e75a959bb4c7a846f21cde65d0a4cd2a713329c7416591c2d182b05e7bd67b4df9895071cac32dc9ab85940459eed8f19660881
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.80.0] - 2026-02-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Syntax Highlighting**: File preview now supports syntax highlighting via `bat`
|
|
14
|
+
- 19 languages supported: Ruby, Python, JS, TS, Go, Rust, Shell, C, C++, Java, SQL, TOML, YAML, JSON, HTML, CSS, Markdown, Dockerfile, Makefile
|
|
15
|
+
- Graceful fallback to plain text when `bat` is not installed
|
|
16
|
+
- mtime-based cache for instant re-display of previously viewed files
|
|
17
|
+
- Health check support (`rufio -c`) now reports `bat` availability
|
|
18
|
+
- **New classes**: `AnsiLineParser`, `SyntaxHighlighter`
|
|
19
|
+
- **New tests**: `test_ansi_line_parser.rb` (25 tests), `test_syntax_highlighter.rb` (+7 async tests)
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Cursor flickering** when navigating source directories: Changed `Renderer#render` from per-line `print` (immediate flush) to a single buffered `write` call — terminal updates are now atomic
|
|
23
|
+
- **Navigation lag** when moving between source files: `bat` is now executed asynchronously in a background thread; the current frame immediately falls back to plain text, then re-renders with highlighting on completion
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **`Renderer#render`**: Replaced row-by-row `print` with single `write(buf)` + `flush` for atomic output
|
|
27
|
+
- **`SyntaxHighlighter`**: Added `highlight_async` (Thread + Mutex + pending guard) alongside existing `highlight`
|
|
28
|
+
- **`TerminalUI`**: Preview caching extended with `highlighted` / `highlighted_wrapped` keys; `@highlight_updated` flag added for async re-render notification
|
|
29
|
+
- **`FilePreview#determine_file_type`**: Added Go, Rust, Shell, TOML, SQL, C, C++, Java, Dockerfile, Makefile language detection
|
|
30
|
+
|
|
31
|
+
### Technical Details
|
|
32
|
+
- **New files**: `lib/rufio/ansi_line_parser.rb`, `lib/rufio/syntax_highlighter.rb`
|
|
33
|
+
- **Modified files**: `lib/rufio/renderer.rb`, `lib/rufio/terminal_ui.rb`, `lib/rufio/file_preview.rb`, `lib/rufio/health_checker.rb`, `lib/rufio/config.rb`, `lib/rufio.rb`
|
|
34
|
+
- **For details**: [CHANGELOG_v0.80.0.md](./docs/CHANGELOG_v0.80.0.md)
|
|
35
|
+
|
|
10
36
|
## [0.71.0] - 2026-02-16
|
|
11
37
|
|
|
12
38
|
### Added
|
|
@@ -611,6 +637,7 @@ For detailed information, see [CHANGELOG_v0.4.0.md](./docs/CHANGELOG_v0.4.0.md)
|
|
|
611
637
|
|
|
612
638
|
### Detailed Release Notes
|
|
613
639
|
|
|
640
|
+
- [v0.80.0](./docs/CHANGELOG_v0.80.0.md) - Syntax Highlighting & Rendering Fixes
|
|
614
641
|
- [v0.31.0](./docs/CHANGELOG_v0.31.0.md) - Experimental Native Scanner Implementation
|
|
615
642
|
- [v0.30.0](./docs/CHANGELOG_v0.30.0.md) - Help System Overhaul
|
|
616
643
|
- [v0.21.0](./docs/CHANGELOG_v0.21.0.md) - Copy Feature & Code Refactoring
|
data/README.md
CHANGED
|
@@ -40,7 +40,7 @@ rufio is not just a file manager. It's a **tool runtime execution environment**.
|
|
|
40
40
|
### As a File Manager
|
|
41
41
|
|
|
42
42
|
- **Vim-like Key Bindings**: Intuitive navigation
|
|
43
|
-
- **Real-time Preview**: Instantly display file contents
|
|
43
|
+
- **Real-time Preview**: Instantly display file contents with syntax highlighting (via `bat`)
|
|
44
44
|
- **Fast Search**: Integration with fzf/rga
|
|
45
45
|
- **Bookmarks**: Quick access to frequently used directories
|
|
46
46
|
- **zoxide Integration**: Smart directory history
|
|
@@ -290,22 +290,41 @@ script_paths:
|
|
|
290
290
|
|
|
291
291
|
## External Tool Integration
|
|
292
292
|
|
|
293
|
-
rufio integrates with the following external tools to extend functionality
|
|
293
|
+
rufio integrates with the following external tools to extend functionality.
|
|
294
|
+
All tools are **optional** — rufio works without them, falling back to built-in behavior.
|
|
294
295
|
|
|
295
|
-
| Tool | Purpose | Key |
|
|
296
|
-
|
|
297
|
-
| fzf | File name search | `s` |
|
|
298
|
-
| rga | File content search | `F` |
|
|
299
|
-
| zoxide | Directory history | `z` |
|
|
296
|
+
| Tool | Purpose | Key | Required? |
|
|
297
|
+
|------|---------|-----|-----------|
|
|
298
|
+
| fzf | File name search | `s` | Optional |
|
|
299
|
+
| rga | File content search | `F` | Optional |
|
|
300
|
+
| zoxide | Directory history | `z` | Optional |
|
|
301
|
+
| bat | Syntax highlighting in preview | — | Optional |
|
|
300
302
|
|
|
301
|
-
###
|
|
303
|
+
### bat — Syntax Highlighting
|
|
304
|
+
|
|
305
|
+
When `bat` is installed, code files in the preview pane are displayed with full syntax
|
|
306
|
+
highlighting (Ruby, Python, Go, Rust, TypeScript, and 15+ more languages).
|
|
307
|
+
|
|
308
|
+
Highlighting is loaded asynchronously — navigation stays fast even on large source trees.
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# macOS
|
|
312
|
+
brew install bat
|
|
313
|
+
|
|
314
|
+
# Ubuntu/Debian
|
|
315
|
+
apt install bat
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Run `rufio -c` to verify bat is detected correctly.
|
|
319
|
+
|
|
320
|
+
### Installation (all tools)
|
|
302
321
|
|
|
303
322
|
```bash
|
|
304
323
|
# macOS
|
|
305
|
-
brew install fzf rga zoxide
|
|
324
|
+
brew install fzf bat rga zoxide
|
|
306
325
|
|
|
307
326
|
# Ubuntu/Debian
|
|
308
|
-
apt install fzf zoxide
|
|
327
|
+
apt install fzf bat zoxide
|
|
309
328
|
# rga requires separate installation: https://github.com/phiresky/ripgrep-all
|
|
310
329
|
```
|
|
311
330
|
|
data/README_ja.md
CHANGED
|
@@ -42,7 +42,7 @@ rufioは単なるファイルマネージャーではありません。**ツー
|
|
|
42
42
|
### ファイルマネージャーとして
|
|
43
43
|
|
|
44
44
|
- **Vimライクなキーバインド**: 直感的なナビゲーション
|
|
45
|
-
- **リアルタイムプレビュー**:
|
|
45
|
+
- **リアルタイムプレビュー**: ファイル内容を即座に表示(`bat` によるシンタックスハイライト対応)
|
|
46
46
|
- **高速検索**: fzf/rgaとの連携
|
|
47
47
|
- **ブックマーク**: よく使うディレクトリに素早くアクセス
|
|
48
48
|
- **zoxide連携**: スマートなディレクトリ履歴
|
|
@@ -255,22 +255,42 @@ SCRIPT_PATHS = [
|
|
|
255
255
|
|
|
256
256
|
## 外部ツール連携
|
|
257
257
|
|
|
258
|
-
rufio
|
|
258
|
+
rufioは以下の外部ツールと連携して機能を拡張します。
|
|
259
|
+
すべて**オプション**です。インストールなしでも rufio は正常動作します。
|
|
259
260
|
|
|
260
|
-
| ツール | 用途 | キー |
|
|
261
|
-
|
|
262
|
-
| fzf | ファイル名検索 | `s` |
|
|
263
|
-
| rga | ファイル内容検索 | `F` |
|
|
264
|
-
| zoxide | ディレクトリ履歴 | `z` |
|
|
261
|
+
| ツール | 用途 | キー | 必須 |
|
|
262
|
+
|--------|------|------|------|
|
|
263
|
+
| fzf | ファイル名検索 | `s` | オプション |
|
|
264
|
+
| rga | ファイル内容検索 | `F` | オプション |
|
|
265
|
+
| zoxide | ディレクトリ履歴 | `z` | オプション |
|
|
266
|
+
| bat | プレビューのシンタックスハイライト | — | オプション |
|
|
265
267
|
|
|
266
|
-
###
|
|
268
|
+
### bat — シンタックスハイライト
|
|
269
|
+
|
|
270
|
+
`bat` をインストールすると、プレビューペインでコードファイルを開いた際に
|
|
271
|
+
シンタックスハイライトが表示されます(Ruby、Python、Go、Rust、TypeScript など15言語以上対応)。
|
|
272
|
+
|
|
273
|
+
ハイライトはバックグラウンドで非同期に読み込まれるため、大きなソースツリーでも
|
|
274
|
+
カーソル移動が重くなりません。
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# macOS
|
|
278
|
+
brew install bat
|
|
279
|
+
|
|
280
|
+
# Ubuntu/Debian
|
|
281
|
+
apt install bat
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`rufio -c` を実行すると bat が正しく認識されているか確認できます。
|
|
285
|
+
|
|
286
|
+
### インストール(全ツール)
|
|
267
287
|
|
|
268
288
|
```bash
|
|
269
289
|
# macOS
|
|
270
|
-
brew install fzf rga zoxide
|
|
290
|
+
brew install fzf bat rga zoxide
|
|
271
291
|
|
|
272
292
|
# Ubuntu/Debian
|
|
273
|
-
apt install fzf zoxide
|
|
293
|
+
apt install fzf bat zoxide
|
|
274
294
|
# rgaは別途インストール: https://github.com/phiresky/ripgrep-all
|
|
275
295
|
```
|
|
276
296
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# CHANGELOG v0.80.0
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Adds syntax highlighting to the file preview pane. When `bat` is installed, code files
|
|
6
|
+
are displayed with full ANSI color highlighting. Environments without `bat` fall back
|
|
7
|
+
silently to plain text display.
|
|
8
|
+
|
|
9
|
+
Also fixes **cursor flickering** and **navigation lag** that occurred when browsing
|
|
10
|
+
source code directories with highlighting active.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## New Features
|
|
15
|
+
|
|
16
|
+
### Syntax Highlighting (via `bat`)
|
|
17
|
+
|
|
18
|
+
The preview pane now renders code files with syntax highlighting using `bat`'s ANSI
|
|
19
|
+
color output.
|
|
20
|
+
|
|
21
|
+
**Supported languages:**
|
|
22
|
+
|
|
23
|
+
| Language | Extensions |
|
|
24
|
+
|----------|------------|
|
|
25
|
+
| Ruby | `.rb` |
|
|
26
|
+
| Python | `.py` |
|
|
27
|
+
| JavaScript | `.js`, `.mjs` |
|
|
28
|
+
| TypeScript | `.ts` |
|
|
29
|
+
| HTML | `.html`, `.htm` |
|
|
30
|
+
| CSS | `.css` |
|
|
31
|
+
| JSON | `.json` |
|
|
32
|
+
| YAML | `.yml`, `.yaml` |
|
|
33
|
+
| Markdown | `.md`, `.markdown` |
|
|
34
|
+
| Go | `.go` |
|
|
35
|
+
| Rust | `.rs` |
|
|
36
|
+
| Shell | `.sh`, `.bash`, `.zsh` |
|
|
37
|
+
| TOML | `.toml` |
|
|
38
|
+
| SQL | `.sql` |
|
|
39
|
+
| C | `.c`, `.h` |
|
|
40
|
+
| C++ | `.cpp`, `.cc`, `.cxx`, `.hpp` |
|
|
41
|
+
| Java | `.java` |
|
|
42
|
+
| Dockerfile | `Dockerfile`, `Dockerfile.*` |
|
|
43
|
+
| Makefile | `Makefile`, `GNUmakefile` |
|
|
44
|
+
|
|
45
|
+
**Behavior:**
|
|
46
|
+
- Graceful degradation — plain text display when `bat` is not installed
|
|
47
|
+
- Non-UTF-8 files (e.g. Shift_JIS) skip highlighting automatically
|
|
48
|
+
- mtime-based cache: second visit to the same file is instant (0 ms)
|
|
49
|
+
- Run `rufio -c` to verify `bat` is detected by the health checker
|
|
50
|
+
|
|
51
|
+
**Installation:**
|
|
52
|
+
```bash
|
|
53
|
+
# macOS
|
|
54
|
+
brew install bat
|
|
55
|
+
|
|
56
|
+
# Ubuntu/Debian
|
|
57
|
+
apt install bat
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Bug Fixes
|
|
63
|
+
|
|
64
|
+
### Fix 1: Cursor Flickering (Renderer Atomic Output)
|
|
65
|
+
|
|
66
|
+
**Symptom:** The preview pane flickered briefly when moving the cursor in source
|
|
67
|
+
code directories.
|
|
68
|
+
|
|
69
|
+
**Root cause:** `Renderer#render` called `print` once per dirty row. With
|
|
70
|
+
`STDOUT sync=true`, each `print` flushes immediately, so intermediate states
|
|
71
|
+
(highlighted color removed, new color not yet drawn) were visible in the terminal.
|
|
72
|
+
|
|
73
|
+
**Fix:** All dirty row output is now accumulated into a single string buffer, then
|
|
74
|
+
written with one `write` + `flush` call — guaranteeing an atomic terminal update.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Before: print row0 → flush → print row1 → flush → ... (intermediate states visible)
|
|
78
|
+
After: buf = row0 + row1 + ... → write(buf) + flush (single atomic update)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Fix 2: Navigation Lag (Async bat Execution)
|
|
82
|
+
|
|
83
|
+
**Symptom:** Moving the cursor to a new file in a source directory caused 10–30 ms
|
|
84
|
+
of lag, making navigation feel sluggish.
|
|
85
|
+
|
|
86
|
+
**Root cause:** `SyntaxHighlighter#highlight` called `IO.popen(['bat', ...])` synchronously
|
|
87
|
+
inside the main loop. The bat process startup cost blocked the frame on every new file visit.
|
|
88
|
+
|
|
89
|
+
**Fix:** Added `highlight_async` which runs `bat` in a background Thread.
|
|
90
|
+
|
|
91
|
+
- The frame immediately after moving to a new file displays plain text (instant fallback)
|
|
92
|
+
- When the background thread completes, it sets `@highlight_updated = true`
|
|
93
|
+
- The main loop detects the flag and triggers a re-render with highlighting
|
|
94
|
+
- A pending guard prevents duplicate threads for the same file path
|
|
95
|
+
- A `Mutex` protects all cache reads/writes for thread safety
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Technical Details
|
|
100
|
+
|
|
101
|
+
### New Files
|
|
102
|
+
|
|
103
|
+
| File | Description |
|
|
104
|
+
|------|-------------|
|
|
105
|
+
| `lib/rufio/ansi_line_parser.rb` | Parses ANSI SGR escape sequences into token arrays. Full-width character-aware wrapping |
|
|
106
|
+
| `lib/rufio/syntax_highlighter.rb` | Wraps the `bat` command. mtime cache, async execution, Mutex protection |
|
|
107
|
+
| `test/test_ansi_line_parser.rb` | Unit tests for AnsiLineParser (25 tests) |
|
|
108
|
+
| `test/test_syntax_highlighter.rb` | Unit tests for SyntaxHighlighter (16 tests) |
|
|
109
|
+
|
|
110
|
+
### Modified Files
|
|
111
|
+
|
|
112
|
+
| File | Change |
|
|
113
|
+
|------|--------|
|
|
114
|
+
| `lib/rufio/renderer.rb` | Per-line `print` → single `write(buf)` + `flush` |
|
|
115
|
+
| `lib/rufio/terminal_ui.rb` | Added `@syntax_highlighter`, highlighting branch in `draw_file_preview_to_buffer`, `@highlight_updated` check in main loop |
|
|
116
|
+
| `lib/rufio/file_preview.rb` | Extended `determine_file_type` with Go, Rust, Shell, TOML, SQL, C, C++, Java, Dockerfile, Makefile |
|
|
117
|
+
| `lib/rufio/health_checker.rb` | Added `check_bat` method |
|
|
118
|
+
| `lib/rufio/config.rb` | Added `health.bat` message key (EN + JA) |
|
|
119
|
+
| `lib/rufio.rb` | Added `require` for `ansi_line_parser` and `syntax_highlighter` |
|
|
120
|
+
| `test/test_renderer.rb` | Added `OutputSpy` helper, 2 new atomic output tests |
|
|
121
|
+
|
|
122
|
+
### Architecture
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
bat (external process)
|
|
126
|
+
↓ IO.popen — background Thread
|
|
127
|
+
SyntaxHighlighter#highlight_async
|
|
128
|
+
↓ callback on completion
|
|
129
|
+
@preview_cache[path][:highlighted] = lines # store ANSI line array
|
|
130
|
+
@highlight_updated = true # notify main loop
|
|
131
|
+
↓ next frame
|
|
132
|
+
AnsiLineParser.parse(line) # ANSI → token array
|
|
133
|
+
AnsiLineParser.wrap(tokens, width) # full-width-aware wrapping
|
|
134
|
+
draw_highlighted_line_to_buffer(screen, ...) # per-char fg: color drawing
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Preview Cache Structure
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
@preview_cache[file_path] = {
|
|
141
|
+
content: Array<String>, # plain text lines
|
|
142
|
+
preview_data: Hash, # type, encoding, etc.
|
|
143
|
+
highlighted: nil | false | Array<String>,
|
|
144
|
+
# nil = not yet requested
|
|
145
|
+
# false = requested, awaiting background result
|
|
146
|
+
# Array = ANSI lines ready to render
|
|
147
|
+
wrapped: Hash, # width => wrapped plain lines
|
|
148
|
+
highlighted_wrapped: Hash # width => wrapped token arrays
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Tests
|
|
155
|
+
|
|
156
|
+
All tests pass (pre-existing TestUISnapshot / TestBufferParity snapshot mismatches excluded):
|
|
157
|
+
|
|
158
|
+
| Test file | Tests | Status |
|
|
159
|
+
|-----------|-------|--------|
|
|
160
|
+
| `test_ansi_line_parser.rb` | 25 | new |
|
|
161
|
+
| `test_syntax_highlighter.rb` | 16 (9 existing + 7 new) | pass |
|
|
162
|
+
| `test_renderer.rb` | 12 (10 existing + 2 new) | pass |
|
|
163
|
+
|
|
164
|
+
**34 new tests** added in this release.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Dependencies
|
|
169
|
+
|
|
170
|
+
### New Optional External Tool
|
|
171
|
+
|
|
172
|
+
| Tool | Purpose | Install |
|
|
173
|
+
|------|---------|---------|
|
|
174
|
+
| `bat` | Syntax highlighting | `brew install bat` / `apt install bat` |
|
|
175
|
+
|
|
176
|
+
rufio works normally without `bat` — plain text preview is always available as a fallback.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Health Check
|
|
181
|
+
|
|
182
|
+
Use `rufio -c` to verify `bat` installation:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
rufio Health Check
|
|
186
|
+
✓ bat (syntax highlight): bat 0.25.0 (2024-...)
|
|
187
|
+
✗ bat (syntax highlight): not found
|
|
188
|
+
brew install bat # optional: enables syntax highlighting
|
|
189
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# ANSI エスケープコード付きの行をトークン列に分解するモジュール。
|
|
5
|
+
# bat --color=always の出力をシンタックスハイライト表示するために使用する。
|
|
6
|
+
module AnsiLineParser
|
|
7
|
+
# SGR (Select Graphic Rendition) ANSI エスケープシーケンスにマッチするパターン
|
|
8
|
+
ANSI_SGR_PATTERN = /\e\[[0-9;]*m/
|
|
9
|
+
|
|
10
|
+
# ANSI 付き行を {text: String, fg: String|nil} のトークン配列に分解する。
|
|
11
|
+
#
|
|
12
|
+
# @param line [String] ANSI コードを含む可能性がある行
|
|
13
|
+
# @return [Array<Hash>] {text:, fg:} のトークン配列
|
|
14
|
+
def self.parse(line)
|
|
15
|
+
tokens = []
|
|
16
|
+
current_fg = nil
|
|
17
|
+
|
|
18
|
+
# ANSI SGR シーケンス、非エスケープ文字列、孤立エスケープの順にスキャン
|
|
19
|
+
line.scan(/#{ANSI_SGR_PATTERN}|[^\e]+|\e(?!\[)/) do |part|
|
|
20
|
+
if part.start_with?("\e[")
|
|
21
|
+
current_fg = apply_ansi_sequence(current_fg, part)
|
|
22
|
+
else
|
|
23
|
+
tokens << { text: part, fg: current_fg }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
tokens
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# トークン配列の表示幅(ANSI コードを除く)を計算する。
|
|
31
|
+
#
|
|
32
|
+
# @param tokens [Array<Hash>] parse が返したトークン配列
|
|
33
|
+
# @return [Integer] 表示幅
|
|
34
|
+
def self.display_width(tokens)
|
|
35
|
+
tokens.sum { |t| TextUtils.display_width(t[:text]) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# トークン配列を max_width で折り返し、行ごとのトークン配列の配列を返す。
|
|
39
|
+
# 全角文字(日本語等)は幅2として扱う。
|
|
40
|
+
#
|
|
41
|
+
# @param tokens [Array<Hash>] parse が返したトークン配列
|
|
42
|
+
# @param max_width [Integer] 折り返し幅(表示幅基準)
|
|
43
|
+
# @return [Array<Array<Hash>>] 行ごとのトークン配列
|
|
44
|
+
def self.wrap(tokens, max_width)
|
|
45
|
+
return [] if tokens.empty? || max_width <= 0
|
|
46
|
+
|
|
47
|
+
lines = []
|
|
48
|
+
current_line = []
|
|
49
|
+
current_width = 0
|
|
50
|
+
|
|
51
|
+
tokens.each do |token|
|
|
52
|
+
fg = token[:fg]
|
|
53
|
+
current_text = String.new
|
|
54
|
+
|
|
55
|
+
token[:text].each_char do |char|
|
|
56
|
+
char_w = TextUtils.char_width(char)
|
|
57
|
+
|
|
58
|
+
if current_width + char_w > max_width
|
|
59
|
+
# 折り返し: 現在のテキストをトークンとして確定
|
|
60
|
+
current_line << { text: current_text, fg: fg } unless current_text.empty?
|
|
61
|
+
lines << current_line
|
|
62
|
+
current_line = []
|
|
63
|
+
current_text = String.new
|
|
64
|
+
current_width = 0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
current_text << char
|
|
68
|
+
current_width += char_w
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
current_line << { text: current_text, fg: fg } unless current_text.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
lines << current_line unless current_line.empty?
|
|
75
|
+
lines
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# private helper: ANSI シーケンスを適用して新しい fg 状態を返す。
|
|
79
|
+
# リセットシーケンスは nil を返す。
|
|
80
|
+
# 複合シーケンス(例: \e[0;32m)はリセット後の色だけを返す。
|
|
81
|
+
def self.apply_ansi_sequence(current_fg, seq)
|
|
82
|
+
# \e[ と m の間のコード文字列を取り出す
|
|
83
|
+
codes_str = seq[2..-2]
|
|
84
|
+
codes = codes_str.split(';')
|
|
85
|
+
|
|
86
|
+
# すべてのコードがリセット("0" or "")ならリセット
|
|
87
|
+
if codes.empty? || codes.all? { |c| c.empty? || c == '0' }
|
|
88
|
+
return nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 先頭がリセットコードで後続に色指定がある場合(例: \e[0;32m)
|
|
92
|
+
if codes.first.empty? || codes.first == '0'
|
|
93
|
+
remaining = codes.drop_while { |c| c.empty? || c == '0' }
|
|
94
|
+
return remaining.empty? ? nil : "\e[#{remaining.join(';')}m"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# 通常の色/属性コード → そのまま使用
|
|
98
|
+
seq
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private_class_method :apply_ansi_sequence
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/rufio/config.rb
CHANGED
|
@@ -66,6 +66,7 @@ module Rufio
|
|
|
66
66
|
'health.fzf' => 'fzf (file search)',
|
|
67
67
|
'health.rga' => 'rga (content search)',
|
|
68
68
|
'health.zoxide' => 'zoxide (directory history)',
|
|
69
|
+
'health.bat' => 'bat (syntax highlight)',
|
|
69
70
|
'health.file_opener' => 'System file opener',
|
|
70
71
|
'health.summary' => 'Summary:',
|
|
71
72
|
'health.ok' => 'OK',
|
|
@@ -135,6 +136,7 @@ module Rufio
|
|
|
135
136
|
'health.fzf' => 'fzf (file search)',
|
|
136
137
|
'health.rga' => 'rga (content search)',
|
|
137
138
|
'health.zoxide' => 'zoxide (directory history)',
|
|
139
|
+
'health.bat' => 'bat (syntax highlight)',
|
|
138
140
|
'health.file_opener' => 'System file opener',
|
|
139
141
|
'health.summary' => 'Summary:',
|
|
140
142
|
'health.ok' => 'OK',
|
data/lib/rufio/file_preview.rb
CHANGED
|
@@ -127,7 +127,16 @@ module Rufio
|
|
|
127
127
|
|
|
128
128
|
def determine_file_type(file_path)
|
|
129
129
|
extension = File.extname(file_path).downcase
|
|
130
|
-
|
|
130
|
+
basename = File.basename(file_path)
|
|
131
|
+
|
|
132
|
+
# 拡張子なしファイル名での判定(Dockerfile 等)
|
|
133
|
+
case basename
|
|
134
|
+
when "Dockerfile", /\ADockerfile\./
|
|
135
|
+
return { type: "code", language: "dockerfile" }
|
|
136
|
+
when "Makefile", "GNUmakefile"
|
|
137
|
+
return { type: "code", language: "makefile" }
|
|
138
|
+
end
|
|
139
|
+
|
|
131
140
|
case extension
|
|
132
141
|
when ".rb"
|
|
133
142
|
{ type: "code", language: "ruby" }
|
|
@@ -147,6 +156,22 @@ module Rufio
|
|
|
147
156
|
{ type: "code", language: "yaml" }
|
|
148
157
|
when ".md", ".markdown"
|
|
149
158
|
{ type: "code", language: "markdown" }
|
|
159
|
+
when ".go"
|
|
160
|
+
{ type: "code", language: "go" }
|
|
161
|
+
when ".rs"
|
|
162
|
+
{ type: "code", language: "rust" }
|
|
163
|
+
when ".sh", ".bash", ".zsh"
|
|
164
|
+
{ type: "code", language: "shell" }
|
|
165
|
+
when ".toml"
|
|
166
|
+
{ type: "code", language: "toml" }
|
|
167
|
+
when ".sql"
|
|
168
|
+
{ type: "code", language: "sql" }
|
|
169
|
+
when ".c", ".h"
|
|
170
|
+
{ type: "code", language: "c" }
|
|
171
|
+
when ".cpp", ".cc", ".cxx", ".hpp"
|
|
172
|
+
{ type: "code", language: "cpp" }
|
|
173
|
+
when ".java"
|
|
174
|
+
{ type: "code", language: "java" }
|
|
150
175
|
when ".txt", ".log"
|
|
151
176
|
{ type: "text", language: nil }
|
|
152
177
|
when ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z"
|
data/lib/rufio/health_checker.rb
CHANGED
|
@@ -21,6 +21,7 @@ module Rufio
|
|
|
21
21
|
{ name: ConfigLoader.message('health.fzf'), method: :check_fzf },
|
|
22
22
|
{ name: ConfigLoader.message('health.rga'), method: :check_rga },
|
|
23
23
|
{ name: ConfigLoader.message('health.zoxide'), method: :check_zoxide },
|
|
24
|
+
{ name: ConfigLoader.message('health.bat'), method: :check_bat },
|
|
24
25
|
{ name: ConfigLoader.message('health.file_opener'), method: :check_file_opener }
|
|
25
26
|
]
|
|
26
27
|
|
|
@@ -136,6 +137,23 @@ module Rufio
|
|
|
136
137
|
end
|
|
137
138
|
end
|
|
138
139
|
|
|
140
|
+
def check_bat
|
|
141
|
+
if system("which bat > /dev/null 2>&1")
|
|
142
|
+
version = `bat --version 2>/dev/null`.strip
|
|
143
|
+
{
|
|
144
|
+
status: :ok,
|
|
145
|
+
message: version,
|
|
146
|
+
details: nil
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
{
|
|
150
|
+
status: :warning,
|
|
151
|
+
message: "bat #{ConfigLoader.message('health.tool_not_found')}",
|
|
152
|
+
details: install_instruction_for('bat')
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
139
157
|
def check_file_opener
|
|
140
158
|
case RUBY_PLATFORM
|
|
141
159
|
when /darwin/
|
|
@@ -180,6 +198,8 @@ module Rufio
|
|
|
180
198
|
"#{ConfigLoader.message('health.install_brew')} rga"
|
|
181
199
|
when 'zoxide'
|
|
182
200
|
"#{ConfigLoader.message('health.install_brew')} zoxide"
|
|
201
|
+
when 'bat'
|
|
202
|
+
"#{ConfigLoader.message('health.install_brew')} bat # optional: syntax highlight"
|
|
183
203
|
end
|
|
184
204
|
when /linux/
|
|
185
205
|
case tool
|
|
@@ -189,6 +209,8 @@ module Rufio
|
|
|
189
209
|
ConfigLoader.message('health.rga_releases')
|
|
190
210
|
when 'zoxide'
|
|
191
211
|
"#{ConfigLoader.message('health.install_apt')} zoxide (Ubuntu/Debian) or check your package manager"
|
|
212
|
+
when 'bat'
|
|
213
|
+
"#{ConfigLoader.message('health.install_apt')} bat (Ubuntu/Debian) or check your package manager # optional: syntax highlight"
|
|
192
214
|
end
|
|
193
215
|
else
|
|
194
216
|
ConfigLoader.message('health.install_guide')
|
data/lib/rufio/renderer.rb
CHANGED
|
@@ -31,13 +31,17 @@ module Rufio
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Phase1: Only process dirty rows (rows that have changed)
|
|
34
|
+
# 全dirty rowsの出力を1つのバッファに積んでから単一の write() で書き出す。
|
|
35
|
+
# STDOUT sync=true 環境で print を行ごとに呼ぶと各行で即座にフラッシュされ
|
|
36
|
+
# 中間状態が表示されてちらつきが発生するため、アトミックな更新を保証する。
|
|
37
|
+
buf = String.new("")
|
|
34
38
|
rendered_count = 0
|
|
35
39
|
dirty.each do |y|
|
|
36
40
|
line = screen.row(y)
|
|
37
41
|
next if line == @front[y] # Skip if content is actually the same
|
|
38
42
|
|
|
39
|
-
# Move cursor to line y (1-indexed) and
|
|
40
|
-
|
|
43
|
+
# Move cursor to line y (1-indexed) and buffer the line
|
|
44
|
+
buf << "\e[#{y + 1};1H#{line}"
|
|
41
45
|
@front[y] = line
|
|
42
46
|
rendered_count += 1
|
|
43
47
|
end
|
|
@@ -45,8 +49,11 @@ module Rufio
|
|
|
45
49
|
# Phase1: Clear dirty tracking after rendering
|
|
46
50
|
screen.clear_dirty
|
|
47
51
|
|
|
48
|
-
#
|
|
49
|
-
|
|
52
|
+
# 単一の write() でアトミックに出力し、その後 flush する
|
|
53
|
+
if rendered_count > 0
|
|
54
|
+
@output.write(buf)
|
|
55
|
+
@output.flush
|
|
56
|
+
end
|
|
50
57
|
|
|
51
58
|
true
|
|
52
59
|
end
|
data/lib/rufio/screen.rb
CHANGED
|
@@ -114,7 +114,8 @@ module Rufio
|
|
|
114
114
|
return " " * @width if y < 0 || y >= @height
|
|
115
115
|
|
|
116
116
|
# Phase1: Pre-allocate string capacity for better performance
|
|
117
|
-
|
|
117
|
+
# String.new("", capacity: N) inherits UTF-8 encoding from ""; String.new(capacity: N) creates ASCII-8BIT
|
|
118
|
+
result = String.new("", capacity: @width * 20)
|
|
118
119
|
current_width = 0 # Phase1: Accumulate width from cells (no recalculation)
|
|
119
120
|
|
|
120
121
|
# オーバーレイがある場合はベース + オーバーレイを合成
|
|
@@ -281,7 +282,7 @@ module Rufio
|
|
|
281
282
|
return char if fg.nil? && bg.nil?
|
|
282
283
|
|
|
283
284
|
# Phase1: String builder with pre-allocated capacity (no array generation)
|
|
284
|
-
result = String.new(capacity: 30)
|
|
285
|
+
result = String.new("", capacity: 30)
|
|
285
286
|
result << fg if fg
|
|
286
287
|
result << bg if bg
|
|
287
288
|
result << char
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# bat コマンドを使ってファイルのシンタックスハイライトを行うクラス。
|
|
5
|
+
# bat が存在しない場合は available? が false を返し、highlight は [] を返す。
|
|
6
|
+
# mtime ベースのキャッシュを持ち、同じファイルの2回目以降は0msで返す。
|
|
7
|
+
class SyntaxHighlighter
|
|
8
|
+
def initialize
|
|
9
|
+
@bat_available = bat_available?
|
|
10
|
+
@cache = {} # file_path => { mtime: Time, lines: Array<String> }
|
|
11
|
+
@pending = {} # file_path => true(非同期実行中フラグ)
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# bat コマンドが利用可能かどうか
|
|
16
|
+
#
|
|
17
|
+
# @return [Boolean]
|
|
18
|
+
def available?
|
|
19
|
+
@bat_available
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# ファイルをシンタックスハイライトして ANSI 付き行の配列を返す(同期版)。
|
|
23
|
+
# bat が使えない場合、ファイルが存在しない場合は []。
|
|
24
|
+
#
|
|
25
|
+
# @param file_path [String] ハイライト対象のファイルパス
|
|
26
|
+
# @param max_lines [Integer] 取得する最大行数
|
|
27
|
+
# @return [Array<String>] ANSI エスケープコード付きの行配列
|
|
28
|
+
def highlight(file_path, max_lines: 50)
|
|
29
|
+
return [] unless @bat_available
|
|
30
|
+
return [] unless File.exist?(file_path)
|
|
31
|
+
|
|
32
|
+
mtime = File.mtime(file_path)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
cache = @cache[file_path]
|
|
35
|
+
return cache[:lines] if cache && cache[:mtime] == mtime
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
lines = run_bat(file_path, max_lines)
|
|
39
|
+
@mutex.synchronize { @cache[file_path] = { mtime: mtime, lines: lines } }
|
|
40
|
+
lines
|
|
41
|
+
rescue => _e
|
|
42
|
+
[]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ファイルをバックグラウンドスレッドでシンタックスハイライトする(非同期版)。
|
|
46
|
+
# メインループをブロックせず、bat 完了時にコールバックを呼ぶ。
|
|
47
|
+
# キャッシュヒット時はコールバックを即時(同期的に)呼ぶ。
|
|
48
|
+
# 同一ファイルへの重複呼び出しは無視する(ペンディングガード)。
|
|
49
|
+
#
|
|
50
|
+
# @param file_path [String] ハイライト対象のファイルパス
|
|
51
|
+
# @param max_lines [Integer] 取得する最大行数
|
|
52
|
+
# @yieldparam lines [Array<String>] ANSI付き行配列(エラー時は [])
|
|
53
|
+
def highlight_async(file_path, max_lines: 50, &callback)
|
|
54
|
+
return unless @bat_available
|
|
55
|
+
return unless File.exist?(file_path)
|
|
56
|
+
|
|
57
|
+
mtime = File.mtime(file_path)
|
|
58
|
+
|
|
59
|
+
@mutex.synchronize do
|
|
60
|
+
# キャッシュヒット → 即時コールバック
|
|
61
|
+
cache = @cache[file_path]
|
|
62
|
+
if cache && cache[:mtime] == mtime
|
|
63
|
+
callback&.call(cache[:lines])
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 既にペンディング中 → 重複スレッドを立てない
|
|
68
|
+
return if @pending[file_path]
|
|
69
|
+
|
|
70
|
+
@pending[file_path] = true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
Thread.new do
|
|
74
|
+
begin
|
|
75
|
+
lines = run_bat(file_path, max_lines)
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
@cache[file_path] = { mtime: mtime, lines: lines }
|
|
78
|
+
@pending.delete(file_path)
|
|
79
|
+
end
|
|
80
|
+
callback&.call(lines)
|
|
81
|
+
rescue => _e
|
|
82
|
+
@mutex.synchronize { @pending.delete(file_path) }
|
|
83
|
+
callback&.call([])
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def bat_available?
|
|
93
|
+
system('which bat > /dev/null 2>&1')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def run_bat(file_path, max_lines)
|
|
97
|
+
output = IO.popen(
|
|
98
|
+
['bat', '--color=always', '--plain', '--line-range', "1:#{max_lines}", file_path],
|
|
99
|
+
err: File::NULL
|
|
100
|
+
) { |io| io.read }
|
|
101
|
+
|
|
102
|
+
return [] unless output
|
|
103
|
+
|
|
104
|
+
output
|
|
105
|
+
.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
|
|
106
|
+
.split("\n")
|
|
107
|
+
rescue => _e
|
|
108
|
+
[]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/rufio/terminal_ui.rb
CHANGED
|
@@ -66,6 +66,11 @@ module Rufio
|
|
|
66
66
|
@preview_cache = {}
|
|
67
67
|
@last_preview_path = nil
|
|
68
68
|
|
|
69
|
+
# シンタックスハイライター(bat が利用可能な場合のみ動作)
|
|
70
|
+
@syntax_highlighter = SyntaxHighlighter.new
|
|
71
|
+
# 非同期ハイライト完了フラグ(Thread → メインループへの通知)
|
|
72
|
+
@highlight_updated = false
|
|
73
|
+
|
|
69
74
|
# Footer cache (bookmark list)
|
|
70
75
|
@cached_bookmarks = nil
|
|
71
76
|
@cached_bookmark_time = nil
|
|
@@ -298,6 +303,12 @@ module Rufio
|
|
|
298
303
|
needs_redraw = true
|
|
299
304
|
end
|
|
300
305
|
|
|
306
|
+
# 非同期シンタックスハイライト完了チェック(バックグラウンドスレッドからの通知)
|
|
307
|
+
if @highlight_updated
|
|
308
|
+
@highlight_updated = false
|
|
309
|
+
needs_redraw = true
|
|
310
|
+
end
|
|
311
|
+
|
|
301
312
|
# DRAW & RENDER phase - 変更があった場合のみ描画
|
|
302
313
|
if needs_redraw
|
|
303
314
|
# Screenバッファに描画(clearは呼ばない。必要な部分だけ更新)
|
|
@@ -829,14 +840,19 @@ module Rufio
|
|
|
829
840
|
# プレビューコンテンツをキャッシュから取得(毎フレームのファイルI/Oを回避)
|
|
830
841
|
preview_content = nil
|
|
831
842
|
wrapped_lines = nil
|
|
843
|
+
highlighted_wrapped_lines = nil
|
|
832
844
|
|
|
833
845
|
if selected_entry && selected_entry[:type] == 'file'
|
|
834
846
|
# キャッシュチェック: 選択ファイルが変わった場合のみプレビューを更新
|
|
835
847
|
if @last_preview_path != selected_entry[:path]
|
|
836
|
-
|
|
848
|
+
full_preview = @file_preview.preview_file(selected_entry[:path])
|
|
849
|
+
preview_content = extract_preview_lines(full_preview)
|
|
837
850
|
@preview_cache[selected_entry[:path]] = {
|
|
838
851
|
content: preview_content,
|
|
839
|
-
|
|
852
|
+
preview_data: full_preview,
|
|
853
|
+
highlighted: nil, # nil = 未取得
|
|
854
|
+
wrapped: {},
|
|
855
|
+
highlighted_wrapped: {}
|
|
840
856
|
}
|
|
841
857
|
@last_preview_path = selected_entry[:path]
|
|
842
858
|
else
|
|
@@ -845,8 +861,49 @@ module Rufio
|
|
|
845
861
|
preview_content = cache_entry[:content] if cache_entry
|
|
846
862
|
end
|
|
847
863
|
|
|
848
|
-
#
|
|
849
|
-
if
|
|
864
|
+
# bat が利用可能な場合はシンタックスハイライトを取得(非同期)
|
|
865
|
+
if @syntax_highlighter&.available? && preview_content
|
|
866
|
+
cache_entry = @preview_cache[selected_entry[:path]]
|
|
867
|
+
if cache_entry
|
|
868
|
+
preview_data = cache_entry[:preview_data]
|
|
869
|
+
if preview_data && preview_data[:type] == 'code' && preview_data[:encoding] == 'UTF-8'
|
|
870
|
+
# ハイライト行を未取得なら非同期で bat を呼び出す
|
|
871
|
+
# nil = 未リクエスト、false = リクエスト済み(結果待ち)、Array = 取得済み
|
|
872
|
+
if cache_entry[:highlighted].nil?
|
|
873
|
+
# 即座に false をセットしてペンディング状態にする(重複リクエスト防止)
|
|
874
|
+
cache_entry[:highlighted] = false
|
|
875
|
+
file_path = selected_entry[:path]
|
|
876
|
+
@syntax_highlighter.highlight_async(file_path) do |lines|
|
|
877
|
+
# バックグラウンドスレッドからキャッシュを更新
|
|
878
|
+
if (ce = @preview_cache[file_path])
|
|
879
|
+
ce[:highlighted] = lines
|
|
880
|
+
ce[:highlighted_wrapped] = {} # 折り返しキャッシュをクリア
|
|
881
|
+
end
|
|
882
|
+
@highlight_updated = true # メインループに再描画を通知
|
|
883
|
+
end
|
|
884
|
+
# このフレームはプレーンテキストで表示(次フレームでハイライト表示)
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
highlighted = cache_entry[:highlighted]
|
|
888
|
+
if highlighted.is_a?(Array) && !highlighted.empty? && safe_width > 0
|
|
889
|
+
if cache_entry[:highlighted_wrapped][safe_width]
|
|
890
|
+
highlighted_wrapped_lines = cache_entry[:highlighted_wrapped][safe_width]
|
|
891
|
+
else
|
|
892
|
+
# 各ハイライト行をトークン化して折り返す
|
|
893
|
+
hl_wrapped = highlighted.flat_map do |hl_line|
|
|
894
|
+
tokens = AnsiLineParser.parse(hl_line)
|
|
895
|
+
tokens.empty? ? [[]] : AnsiLineParser.wrap(tokens, safe_width - 1)
|
|
896
|
+
end
|
|
897
|
+
cache_entry[:highlighted_wrapped][safe_width] = hl_wrapped
|
|
898
|
+
highlighted_wrapped_lines = hl_wrapped
|
|
899
|
+
end
|
|
900
|
+
end
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# プレーンテキストの折り返し(ハイライトなしのフォールバック)
|
|
906
|
+
if preview_content && safe_width > 0 && highlighted_wrapped_lines.nil?
|
|
850
907
|
cache_entry = @preview_cache[selected_entry[:path]]
|
|
851
908
|
if cache_entry && cache_entry[:wrapped][safe_width]
|
|
852
909
|
wrapped_lines = cache_entry[:wrapped][safe_width]
|
|
@@ -857,48 +914,55 @@ module Rufio
|
|
|
857
914
|
end
|
|
858
915
|
end
|
|
859
916
|
|
|
917
|
+
content_x = cursor_position + 1
|
|
918
|
+
|
|
860
919
|
(0...height).each do |i|
|
|
861
920
|
line_num = i + CONTENT_START_LINE
|
|
862
921
|
|
|
863
922
|
# 区切り線
|
|
864
923
|
screen.put(cursor_position, line_num, '│')
|
|
865
924
|
|
|
866
|
-
|
|
925
|
+
next if safe_width <= 0
|
|
867
926
|
|
|
868
927
|
if selected_entry && i == 0
|
|
869
928
|
# プレビューヘッダー
|
|
870
929
|
header = " #{selected_entry[:name]} "
|
|
871
|
-
if @keybind_handler&.preview_focused?
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
930
|
+
header += "[PREVIEW MODE]" if @keybind_handler&.preview_focused?
|
|
931
|
+
header = TextUtils.truncate_to_width(header, safe_width) if TextUtils.display_width(header) > safe_width
|
|
932
|
+
remaining_space = safe_width - TextUtils.display_width(header)
|
|
933
|
+
header += ' ' * remaining_space if remaining_space > 0
|
|
934
|
+
screen.put_string(content_x, line_num, header)
|
|
935
|
+
|
|
936
|
+
elsif i >= 2 && highlighted_wrapped_lines
|
|
937
|
+
# シンタックスハイライト付きコンテンツ
|
|
877
938
|
scroll_offset = @keybind_handler&.preview_scroll_offset || 0
|
|
878
939
|
display_line_index = i - 2 + scroll_offset
|
|
879
940
|
|
|
880
|
-
if display_line_index <
|
|
881
|
-
|
|
882
|
-
|
|
941
|
+
if display_line_index < highlighted_wrapped_lines.length
|
|
942
|
+
draw_highlighted_line_to_buffer(screen, content_x, line_num,
|
|
943
|
+
highlighted_wrapped_lines[display_line_index], safe_width)
|
|
883
944
|
else
|
|
884
|
-
|
|
945
|
+
screen.put_string(content_x, line_num, ' ' * safe_width)
|
|
885
946
|
end
|
|
886
|
-
else
|
|
887
|
-
content_to_print = ' '
|
|
888
|
-
end
|
|
889
947
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
content_to_print = TextUtils.truncate_to_width(content_to_print, safe_width)
|
|
895
|
-
end
|
|
948
|
+
elsif i >= 2 && wrapped_lines
|
|
949
|
+
# プレーンテキストコンテンツ
|
|
950
|
+
scroll_offset = @keybind_handler&.preview_scroll_offset || 0
|
|
951
|
+
display_line_index = i - 2 + scroll_offset
|
|
896
952
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
953
|
+
content_to_print = if display_line_index < wrapped_lines.length
|
|
954
|
+
" #{wrapped_lines[display_line_index] || ''}"
|
|
955
|
+
else
|
|
956
|
+
' '
|
|
957
|
+
end
|
|
958
|
+
content_to_print = TextUtils.truncate_to_width(content_to_print, safe_width) if TextUtils.display_width(content_to_print) > safe_width
|
|
959
|
+
remaining_space = safe_width - TextUtils.display_width(content_to_print)
|
|
960
|
+
content_to_print += ' ' * remaining_space if remaining_space > 0
|
|
961
|
+
screen.put_string(content_x, line_num, content_to_print)
|
|
900
962
|
|
|
901
|
-
|
|
963
|
+
else
|
|
964
|
+
screen.put_string(content_x, line_num, ' ' * safe_width)
|
|
965
|
+
end
|
|
902
966
|
end
|
|
903
967
|
end
|
|
904
968
|
|
|
@@ -994,6 +1058,13 @@ module Rufio
|
|
|
994
1058
|
return [] unless entry && entry[:type] == 'file'
|
|
995
1059
|
|
|
996
1060
|
preview = @file_preview.preview_file(entry[:path])
|
|
1061
|
+
extract_preview_lines(preview)
|
|
1062
|
+
rescue StandardError
|
|
1063
|
+
["(#{ConfigLoader.message('file.preview_error')})"]
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
# FilePreview の結果ハッシュからプレーンテキスト行を抽出する
|
|
1067
|
+
def extract_preview_lines(preview)
|
|
997
1068
|
case preview[:type]
|
|
998
1069
|
when 'text', 'code'
|
|
999
1070
|
preview[:lines]
|
|
@@ -1008,6 +1079,36 @@ module Rufio
|
|
|
1008
1079
|
["(#{ConfigLoader.message('file.preview_error')})"]
|
|
1009
1080
|
end
|
|
1010
1081
|
|
|
1082
|
+
# ハイライト済みトークン列を1行分 Screen バッファに描画する
|
|
1083
|
+
# 先頭に1スペースを追加し、残りをスペースで埋める
|
|
1084
|
+
def draw_highlighted_line_to_buffer(screen, x, y, tokens, max_width)
|
|
1085
|
+
current_x = x
|
|
1086
|
+
max_x = x + max_width
|
|
1087
|
+
|
|
1088
|
+
# 先頭スペース
|
|
1089
|
+
if current_x < max_x
|
|
1090
|
+
screen.put(current_x, y, ' ')
|
|
1091
|
+
current_x += 1
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
# トークンを描画
|
|
1095
|
+
tokens&.each do |token|
|
|
1096
|
+
break if current_x >= max_x
|
|
1097
|
+
token[:text].each_char do |char|
|
|
1098
|
+
char_w = TextUtils.char_width(char)
|
|
1099
|
+
break if current_x + char_w > max_x
|
|
1100
|
+
screen.put(current_x, y, char, fg: token[:fg])
|
|
1101
|
+
current_x += char_w
|
|
1102
|
+
end
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
# 残りをスペースで埋める
|
|
1106
|
+
while current_x < max_x
|
|
1107
|
+
screen.put(current_x, y, ' ')
|
|
1108
|
+
current_x += 1
|
|
1109
|
+
end
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1011
1112
|
|
|
1012
1113
|
def get_display_entries
|
|
1013
1114
|
entries = if @keybind_handler.filter_active?
|
data/lib/rufio/version.rb
CHANGED
data/lib/rufio.rb
CHANGED
|
@@ -16,6 +16,8 @@ require_relative 'rufio/dialog_renderer'
|
|
|
16
16
|
require_relative 'rufio/text_utils'
|
|
17
17
|
require_relative 'rufio/logger'
|
|
18
18
|
require_relative 'rufio/keybind_handler'
|
|
19
|
+
require_relative 'rufio/ansi_line_parser'
|
|
20
|
+
require_relative 'rufio/syntax_highlighter'
|
|
19
21
|
require_relative 'rufio/file_preview'
|
|
20
22
|
require_relative 'rufio/terminal_ui'
|
|
21
23
|
require_relative 'rufio/application'
|
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: 0.
|
|
4
|
+
version: 0.80.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-02-
|
|
11
|
+
date: 2026-02-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: io-console
|
|
@@ -137,6 +137,7 @@ files:
|
|
|
137
137
|
- docs/CHANGELOG_v0.6.0.md
|
|
138
138
|
- docs/CHANGELOG_v0.7.0.md
|
|
139
139
|
- docs/CHANGELOG_v0.8.0.md
|
|
140
|
+
- docs/CHANGELOG_v0.80.0.md
|
|
140
141
|
- docs/CHANGELOG_v0.9.0.md
|
|
141
142
|
- examples/bookmarks.yml
|
|
142
143
|
- examples/config.rb
|
|
@@ -145,6 +146,7 @@ files:
|
|
|
145
146
|
- info/keybindings.md
|
|
146
147
|
- info/welcome.md
|
|
147
148
|
- lib/rufio.rb
|
|
149
|
+
- lib/rufio/ansi_line_parser.rb
|
|
148
150
|
- lib/rufio/application.rb
|
|
149
151
|
- lib/rufio/async_scanner_fiber.rb
|
|
150
152
|
- lib/rufio/async_scanner_promise.rb
|
|
@@ -191,6 +193,7 @@ files:
|
|
|
191
193
|
- lib/rufio/script_runner.rb
|
|
192
194
|
- lib/rufio/selection_manager.rb
|
|
193
195
|
- lib/rufio/shell_command_completion.rb
|
|
196
|
+
- lib/rufio/syntax_highlighter.rb
|
|
194
197
|
- lib/rufio/tab_mode_manager.rb
|
|
195
198
|
- lib/rufio/task_status.rb
|
|
196
199
|
- lib/rufio/terminal_ui.rb
|