rufio 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +26 -297
- data/README_ja.md +24 -261
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/docs/guide.md +410 -0
- data/docs/guide_ja.md +410 -0
- data/info/help.md +54 -17
- data/info/keybindings.md +50 -22
- data/info/welcome.md +14 -5
- data/lib/rufio/application.rb +12 -1
- data/lib/rufio/archive_controller.rb +211 -0
- data/lib/rufio/background_command_executor.rb +9 -0
- data/lib/rufio/background_status.rb +49 -0
- data/lib/rufio/bookmark_controller.rb +38 -121
- data/lib/rufio/bookmark_manager.rb +77 -331
- data/lib/rufio/command_completion.rb +28 -2
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +60 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +4 -11
- data/lib/rufio/dialog_renderer.rb +90 -152
- data/lib/rufio/dialog_support.rb +69 -0
- data/lib/rufio/document_text_extractor.rb +167 -0
- data/lib/rufio/file_operation_controller.rb +49 -222
- data/lib/rufio/file_operations.rb +28 -6
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +125 -13
- data/lib/rufio/help_menu.rb +19 -7
- data/lib/rufio/interpreter_resolver.rb +6 -11
- data/lib/rufio/job_manager.rb +2 -5
- data/lib/rufio/key_input.rb +77 -0
- data/lib/rufio/keybind_handler.rb +83 -116
- data/lib/rufio/local_script_scanner.rb +6 -18
- data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native_content_search.rb +153 -0
- data/lib/rufio/native_scanner.rb +13 -68
- data/lib/rufio/native_scanner_zig.rb +106 -56
- data/lib/rufio/native_zip.rb +121 -0
- data/lib/rufio/navigation_controller.rb +20 -26
- data/lib/rufio/path_jump.rb +55 -0
- data/lib/rufio/screen.rb +2 -12
- data/lib/rufio/script_executor.rb +6 -73
- data/lib/rufio/script_file.rb +44 -0
- data/lib/rufio/script_path_manager.rb +9 -43
- data/lib/rufio/script_runner.rb +13 -50
- data/lib/rufio/search_controller.rb +1 -1
- data/lib/rufio/shell_command_completion.rb +1 -5
- data/lib/rufio/shell_init.rb +90 -0
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +84 -298
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +73 -45
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio/zip_archive.rb +314 -0
- data/lib/rufio/zip_extractor.rb +346 -0
- data/lib/rufio/zip_writer.rb +268 -0
- data/lib/rufio/zoxide_integration.rb +5 -43
- data/lib/rufio.rb +6 -0
- data/lib_zig/rufio_native/Makefile +54 -17
- data/lib_zig/rufio_native/bench.sh +36 -0
- data/lib_zig/rufio_native/build.zig +32 -35
- data/lib_zig/rufio_native/src/cli.zig +99 -0
- data/lib_zig/rufio_native/src/main.zig +187 -83
- data/lib_zig/rufio_native/src/os.zig +889 -0
- data/lib_zig/rufio_native/src/search.zig +919 -0
- data/lib_zig/rufio_native/src/search_test.zig +88 -0
- data/lib_zig/rufio_native/src/zip.zig +482 -0
- metadata +128 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
data/info/keybindings.md
CHANGED
|
@@ -9,22 +9,24 @@ Complete reference of all keyboard shortcuts in **rufio**.
|
|
|
9
9
|
| `j` / `Down` | Move down |
|
|
10
10
|
| `k` / `Up` | Move up |
|
|
11
11
|
| `h` / `Left` | Go to parent directory |
|
|
12
|
-
| `l` / `Right` / `Enter` | Enter directory
|
|
12
|
+
| `l` / `Right` / `Enter` | Enter directory |
|
|
13
13
|
| `g` | Jump to top of list |
|
|
14
|
-
| `
|
|
14
|
+
| `}` | Jump to bottom of list |
|
|
15
15
|
|
|
16
16
|
## File Operations
|
|
17
17
|
|
|
18
18
|
| Key | Action |
|
|
19
19
|
|-----|--------|
|
|
20
20
|
| `o` | Open file with external editor |
|
|
21
|
-
| `e` | Open in file explorer
|
|
21
|
+
| `e` | Open current directory in file explorer |
|
|
22
22
|
| `R` | Refresh directory contents |
|
|
23
23
|
| `r` | Rename file or directory |
|
|
24
24
|
| `d` | Delete file or directory (with confirmation) |
|
|
25
25
|
| `a` | Create new file |
|
|
26
26
|
| `A` | Create new directory |
|
|
27
27
|
|
|
28
|
+
`.zip` previews list the archive entries (size, method, modified time, name) without extracting them.
|
|
29
|
+
|
|
28
30
|
## File Selection
|
|
29
31
|
|
|
30
32
|
| Key | Action |
|
|
@@ -33,6 +35,9 @@ Complete reference of all keyboard shortcuts in **rufio**.
|
|
|
33
35
|
| `m` | Move selected items to current directory |
|
|
34
36
|
| `c` | Copy selected items to current directory |
|
|
35
37
|
| `x` | Delete selected items |
|
|
38
|
+
| `z` | Extract selected `.zip` files (each into `<name>/`) / compress selected items into one `.zip` |
|
|
39
|
+
|
|
40
|
+
ZIP extraction and compression run as background jobs. Check progress in Jobs mode (`J`).
|
|
36
41
|
|
|
37
42
|
## Filtering & Search
|
|
38
43
|
|
|
@@ -40,7 +45,9 @@ Complete reference of all keyboard shortcuts in **rufio**.
|
|
|
40
45
|
|-----|--------|
|
|
41
46
|
| `f` | Enter filter mode (type to filter) |
|
|
42
47
|
| `s` | Search file names with fzf |
|
|
48
|
+
| `/` | Search file names with fzf (same as `s`) |
|
|
43
49
|
| `F` | Search file contents with rga |
|
|
50
|
+
| `G` | Global search: file names or contents across all bookmarks |
|
|
44
51
|
|
|
45
52
|
## Bookmarks & Navigation
|
|
46
53
|
|
|
@@ -50,30 +57,33 @@ Complete reference of all keyboard shortcuts in **rufio**.
|
|
|
50
57
|
| `B` | Bookmark menu (view/add/remove/script paths) |
|
|
51
58
|
| `0` | Jump to startup directory |
|
|
52
59
|
| `1-9` | Jump to bookmark 1-9 |
|
|
53
|
-
| `
|
|
60
|
+
| `Tab` | Cycle to next bookmark (Files mode) |
|
|
61
|
+
| `Shift+Tab` | Cycle to previous bookmark (Files mode) |
|
|
62
|
+
| `z` | Navigate using zoxide history (when nothing is selected) |
|
|
54
63
|
|
|
55
|
-
##
|
|
64
|
+
## Modes & Tool Runtime
|
|
56
65
|
|
|
57
66
|
| Key | Action |
|
|
58
67
|
|-----|--------|
|
|
59
68
|
| `:` | Enter command mode |
|
|
69
|
+
| `G` | Global search mode |
|
|
70
|
+
| `C` | Config mode: open `~/.config/rufio/` |
|
|
60
71
|
| `J` | Job mode (view background jobs) |
|
|
61
72
|
| `L` | View execution logs |
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
| Key | Action |
|
|
66
|
-
|-----|--------|
|
|
67
|
-
| `Tab` | Switch mode: Files -> Logs -> Jobs -> Help |
|
|
68
|
-
| `Shift+Tab` | Switch mode (reverse direction) |
|
|
69
|
-
| `?` | Enter help mode |
|
|
70
|
-
| `ESC` | Exit current mode (help/filter/command) |
|
|
71
|
-
| `q` | Quit rufio |
|
|
73
|
+
| `?` | Open the floating help menu |
|
|
74
|
+
| `q` | Quit rufio (with confirmation) |
|
|
75
|
+
| `T` | Quit rufio and cd the shell to the current directory (requires `rufio --shell-init`) |
|
|
72
76
|
|
|
73
77
|
---
|
|
74
78
|
|
|
75
79
|
## Mode-Specific Keys
|
|
76
80
|
|
|
81
|
+
### Preview
|
|
82
|
+
- **P**: Open fullscreen file preview (hides the file list)
|
|
83
|
+
- **j/k**: Scroll preview
|
|
84
|
+
- **Ctrl+D/U**: Page down/up
|
|
85
|
+
- **ESC**: Back to the file list
|
|
86
|
+
|
|
77
87
|
### Filter Mode
|
|
78
88
|
- **Any character**: Add to filter query
|
|
79
89
|
- **Backspace**: Remove last character
|
|
@@ -82,7 +92,7 @@ Complete reference of all keyboard shortcuts in **rufio**.
|
|
|
82
92
|
|
|
83
93
|
### Command Mode
|
|
84
94
|
- **Any character**: Add to command input
|
|
85
|
-
- **Tab**: Auto-complete (scripts, rake tasks, commands)
|
|
95
|
+
- **Tab**: Auto-complete (scripts, rake tasks, commands, paths)
|
|
86
96
|
- **Up/Down**: Navigate command history
|
|
87
97
|
- **Enter**: Execute command
|
|
88
98
|
- **Backspace**: Remove last character
|
|
@@ -92,27 +102,45 @@ Complete reference of all keyboard shortcuts in **rufio**.
|
|
|
92
102
|
- `@script` - Execute a registered or local script
|
|
93
103
|
- `rake:task` - Execute a Rakefile task
|
|
94
104
|
- `!command` - Execute a shell command
|
|
105
|
+
- `cd <path>` - Go to a directory (paste a path; a file path selects the file, `cd -` goes back, `Tab` completes directories)
|
|
95
106
|
- `command` - Execute a built-in or plugin command
|
|
96
107
|
|
|
97
|
-
###
|
|
98
|
-
- **
|
|
99
|
-
- **
|
|
108
|
+
### Global Search (G)
|
|
109
|
+
- **Type**: Filter results (literal, case insensitive)
|
|
110
|
+
- **Tab**: Switch between file names and content
|
|
111
|
+
- **Up/Down** or **Ctrl+J/K or Ctrl+N/P**: Select a result
|
|
112
|
+
- **Enter**: Go to the selected file in Files mode
|
|
113
|
+
- **Ctrl+U**: Clear the query
|
|
114
|
+
- **ESC**: Return to the previous mode
|
|
115
|
+
- Content search also reads `.pdf`, `.xlsx` and `.pptx`
|
|
100
116
|
|
|
101
|
-
###
|
|
117
|
+
### Config Mode (C)
|
|
118
|
+
- Same navigation, preview and file operations as Files mode
|
|
119
|
+
- Leaving `~/.config/rufio/` returns to Files mode
|
|
120
|
+
|
|
121
|
+
### Job Mode (J)
|
|
102
122
|
- **j/k**: Navigate through jobs
|
|
123
|
+
- **g/G**: First / last job
|
|
124
|
+
- **Space**: View job log
|
|
125
|
+
- **`x`: Cancel** the selected job (including ZIP jobs)
|
|
103
126
|
- **ESC**: Exit job mode
|
|
104
127
|
|
|
128
|
+
### Help Menu (?)
|
|
129
|
+
- **1/2/3** or **Tab/Shift+Tab**: Switch section (Controls / Modes / Scripts)
|
|
130
|
+
- **j/k**: Scroll | **Ctrl+D/U**: Page down/up | **g/G**: Top / bottom
|
|
131
|
+
- **ESC**, **q** or **?**: Close and keep your current mode and directory
|
|
132
|
+
|
|
105
133
|
---
|
|
106
134
|
|
|
107
135
|
## Tips
|
|
108
136
|
|
|
109
137
|
- **Vim users**: Navigation keys (`hjkl`) work exactly like Vim
|
|
110
138
|
- **Selection**: Use `Space` to mark multiple files, then operate on all at once
|
|
111
|
-
- **Quick access**: Number keys `1-9` provide instant access to bookmarks
|
|
139
|
+
- **Quick access**: Number keys `1-9` and `Tab` provide instant access to bookmarks
|
|
112
140
|
- **Filtering**: Press `f` and start typing for real-time filtering
|
|
113
141
|
- **Rake tasks**: Type `:rake:` then `Tab` to see available tasks
|
|
114
142
|
- **Local scripts**: Scripts in the current directory are auto-detected with `@` prefix
|
|
115
143
|
|
|
116
144
|
---
|
|
117
145
|
|
|
118
|
-
*Press `?` anytime to
|
|
146
|
+
*Press `?` anytime to open the help menu*
|
data/info/welcome.md
CHANGED
|
@@ -10,6 +10,8 @@ A TUI file manager that executes and coordinates tools and scripts from files.
|
|
|
10
10
|
- **Vim-like Keybindings** - Navigate with `j/k/h/l` keys
|
|
11
11
|
- **Real-time Filtering** - Press `f` to filter files instantly
|
|
12
12
|
- **Advanced Search** - Use `fzf` for file names and `rga` for content search
|
|
13
|
+
- **Global Search** - Press `G` to search names or contents (including PDF/Excel/PowerPoint) across all bookmarks
|
|
14
|
+
- **ZIP Support** - Preview `.zip` contents, extract or compress selected items with `z`
|
|
13
15
|
- **Bookmark Support** - Quick access to favorite directories
|
|
14
16
|
- **Job Management** - Run commands in the background
|
|
15
17
|
- **Plugin System** - Extend functionality with custom plugins
|
|
@@ -24,12 +26,18 @@ A TUI file manager that executes and coordinates tools and scripts from files.
|
|
|
24
26
|
| `s` | Search with fzf |
|
|
25
27
|
| `b` | Add bookmark |
|
|
26
28
|
| `B` | Bookmark menu |
|
|
27
|
-
| `
|
|
29
|
+
| `Tab` | Next bookmark |
|
|
30
|
+
| `z` | zoxide history / extract or compress selected items |
|
|
31
|
+
| `G` | Global search across bookmarks |
|
|
32
|
+
| `C` | Config mode |
|
|
28
33
|
| `:` | Command mode |
|
|
29
34
|
| `J` | Job mode |
|
|
30
|
-
|
|
|
31
|
-
|
|
|
35
|
+
| `L` | Execution logs |
|
|
36
|
+
| `?` | Help menu |
|
|
32
37
|
| `q` | Quit |
|
|
38
|
+
| `T` | Quit and cd to the current directory |
|
|
39
|
+
|
|
40
|
+
In G mode, use `Ctrl+J` / `Ctrl+K` to select the next / previous result and `Enter` to open it.
|
|
33
41
|
|
|
34
42
|
## Command Mode
|
|
35
43
|
|
|
@@ -38,12 +46,13 @@ Press `:` to enter command mode:
|
|
|
38
46
|
- `@script.sh` - Execute a script
|
|
39
47
|
- `rake:test` - Execute a rake task
|
|
40
48
|
- `!git status` - Run a shell command
|
|
49
|
+
- `cd ~/projects` - Go to a pasted path
|
|
41
50
|
- `Tab` - Auto-complete commands
|
|
42
51
|
|
|
43
52
|
## Getting Help
|
|
44
53
|
|
|
45
|
-
Press `?` to
|
|
46
|
-
|
|
54
|
+
Press `?` to open the floating **Help Menu** (Controls / Modes / Scripts).
|
|
55
|
+
Switch sections with `1/2/3` or `Tab`, scroll with `j/k`, and close with `ESC`.
|
|
47
56
|
|
|
48
57
|
## More Information
|
|
49
58
|
|
data/lib/rufio/application.rb
CHANGED
|
@@ -7,8 +7,10 @@ module Rufio
|
|
|
7
7
|
# Error display constants
|
|
8
8
|
BACKTRACE_LINES = 5 # Number of backtrace lines to show
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# choosedir: T キーで終了した時に最後のディレクトリを書き出すファイル(rufio --shell-init の関数が使う)
|
|
11
|
+
def initialize(start_directory = Dir.pwd, choosedir: nil)
|
|
11
12
|
@start_directory = File.expand_path(start_directory)
|
|
13
|
+
@choosedir = choosedir
|
|
12
14
|
# Load configuration including language settings
|
|
13
15
|
ConfigLoader.load_config
|
|
14
16
|
end
|
|
@@ -28,12 +30,21 @@ module Rufio
|
|
|
28
30
|
|
|
29
31
|
# アプリケーション開始
|
|
30
32
|
terminal_ui.start(directory_listing, keybind_handler, file_preview, background_executor)
|
|
33
|
+
write_choosedir(terminal_ui.exit_directory)
|
|
31
34
|
rescue Interrupt
|
|
32
35
|
puts "\n\n#{ConfigLoader.message('app.interrupted')}"
|
|
33
36
|
rescue StandardError => e
|
|
34
37
|
puts "\n#{ConfigLoader.message('app.error_occurred')}: #{e.message}"
|
|
35
38
|
puts e.backtrace.first(BACKTRACE_LINES).join("\n")
|
|
36
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def write_choosedir(directory)
|
|
44
|
+
return unless @choosedir && directory
|
|
45
|
+
|
|
46
|
+
File.write(@choosedir, directory)
|
|
47
|
+
end
|
|
37
48
|
end
|
|
38
49
|
end
|
|
39
50
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'dialog_support'
|
|
4
|
+
require_relative 'zip_extractor'
|
|
5
|
+
require_relative 'zip_writer'
|
|
6
|
+
|
|
7
|
+
module Rufio
|
|
8
|
+
# 選択中のアイテムを z キーで圧縮・展開するコントローラ
|
|
9
|
+
#
|
|
10
|
+
# - 選択がすべて .zip ファイルなら、それぞれを現在のディレクトリの <名前>/ に展開する
|
|
11
|
+
# - それ以外は選択アイテムを 1 つの ZIP にまとめて現在のディレクトリに作成する
|
|
12
|
+
# 処理は JobManager のジョブとしてバックグラウンドで実行し、Jobs モードからキャンセルできる。
|
|
13
|
+
class ArchiveController
|
|
14
|
+
include DialogSupport
|
|
15
|
+
|
|
16
|
+
DEFAULT_ARCHIVE_NAME = 'Archive.zip'
|
|
17
|
+
DIALOG_WIDTH = 50
|
|
18
|
+
DIALOG_BORDER_HEIGHT = 4
|
|
19
|
+
PROGRESS_STEP = 5 # 進捗表示を更新する間隔(%)
|
|
20
|
+
|
|
21
|
+
Plan = Struct.new(:action, :source_dir, :dest_dir, :items, :archives, :default_name, keyword_init: true)
|
|
22
|
+
|
|
23
|
+
def initialize(directory_listing, dialog_renderer, selection_manager, job_manager, notification_manager)
|
|
24
|
+
@directory_listing = directory_listing
|
|
25
|
+
@dialog_renderer = dialog_renderer
|
|
26
|
+
@selection_manager = selection_manager
|
|
27
|
+
@job_manager = job_manager
|
|
28
|
+
@notification_manager = notification_manager
|
|
29
|
+
@terminal_ui = nil
|
|
30
|
+
@mutex = Mutex.new
|
|
31
|
+
@finished_dirs = []
|
|
32
|
+
@progress_changed = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set_terminal_ui(terminal_ui)
|
|
36
|
+
@terminal_ui = terminal_ui
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_directory_listing(directory_listing)
|
|
40
|
+
@directory_listing = directory_listing
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 選択アイテムから実行内容を決める(UI に依存しない)
|
|
44
|
+
def self.plan(source_dir, items, dest_dir)
|
|
45
|
+
items = items.reject { |item| item == '..' }
|
|
46
|
+
return nil if items.empty?
|
|
47
|
+
|
|
48
|
+
paths = items.map { |item| File.join(source_dir, item) }
|
|
49
|
+
if paths.all? { |path| File.file?(path) && File.extname(path).casecmp?('.zip') }
|
|
50
|
+
archives = paths.map { |path| [path, unique_path(dest_dir, File.basename(path, '.*'), '')] }
|
|
51
|
+
Plan.new(action: :extract, source_dir: source_dir, dest_dir: dest_dir, items: items, archives: archives)
|
|
52
|
+
else
|
|
53
|
+
default_name = items.length == 1 ? "#{File.basename(items.first)}.zip" : DEFAULT_ARCHIVE_NAME
|
|
54
|
+
Plan.new(action: :compress, source_dir: source_dir, dest_dir: dest_dir, items: items, default_name: default_name)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# dir/base<ext> が既にあれば "base 2<ext>", "base 3<ext>" ... を返す
|
|
59
|
+
def self.unique_path(dir, base, ext)
|
|
60
|
+
candidate = File.join(dir, "#{base}#{ext}")
|
|
61
|
+
number = 2
|
|
62
|
+
while File.exist?(candidate) || File.symlink?(candidate)
|
|
63
|
+
candidate = File.join(dir, "#{base} #{number}#{ext}")
|
|
64
|
+
number += 1
|
|
65
|
+
end
|
|
66
|
+
candidate
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 入力されたアーカイブ名を検証して .zip を補う(不正なら nil)
|
|
70
|
+
def self.archive_name(input, default_name)
|
|
71
|
+
name = input.to_s.strip
|
|
72
|
+
name = default_name if name.empty?
|
|
73
|
+
return nil if name.match?(%r{[/\\\0]}) || ['.', '..'].include?(name)
|
|
74
|
+
|
|
75
|
+
File.extname(name).casecmp?('.zip') ? name : "#{name}.zip"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# z キー(選択あり)の入口
|
|
79
|
+
def archive_selected
|
|
80
|
+
return false if @selection_manager.empty?
|
|
81
|
+
|
|
82
|
+
current_path = @directory_listing&.current_path || Dir.pwd
|
|
83
|
+
source_dir = @selection_manager.source_directory || current_path
|
|
84
|
+
plan = self.class.plan(source_dir, @selection_manager.selected_items, current_path)
|
|
85
|
+
return false unless plan
|
|
86
|
+
|
|
87
|
+
started = plan.action == :extract ? confirm_and_extract(plan) : prompt_and_compress(plan)
|
|
88
|
+
@selection_manager.clear if started
|
|
89
|
+
@terminal_ui&.refresh_display
|
|
90
|
+
started
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def start_extract(zip_path, dest_dir)
|
|
94
|
+
run_job("Extract #{File.basename(zip_path)}", [File.dirname(dest_dir), dest_dir]) do |job, progress|
|
|
95
|
+
created = !File.exist?(dest_dir)
|
|
96
|
+
begin
|
|
97
|
+
result = ZipExtractor.new(zip_path, dest_dir, progress: progress, cancelled: -> { job.cancelled? }).extract
|
|
98
|
+
rescue StandardError
|
|
99
|
+
# 新しく作った展開先だけを片付ける(既存ディレクトリには触れない)
|
|
100
|
+
FileUtils.rm_rf(dest_dir) if created
|
|
101
|
+
raise
|
|
102
|
+
end
|
|
103
|
+
job.append_log("Extracted #{result.extracted.length} item(s) to #{dest_dir}")
|
|
104
|
+
result.skipped.each { |skip| job.append_log("Skipped #{skip[:name]} (#{skip[:reason]})") }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def start_compress(zip_path, source_dir, items)
|
|
109
|
+
run_job("Compress #{File.basename(zip_path)}", [File.dirname(zip_path)]) do |job, progress|
|
|
110
|
+
result = ZipWriter.new(zip_path, source_dir, items, progress: progress, cancelled: -> { job.cancelled? }).write
|
|
111
|
+
job.append_log("Created #{zip_path} with #{result.added.length} item(s)")
|
|
112
|
+
result.skipped.each { |skip| job.append_log("Skipped #{skip[:name]} (#{skip[:reason]})") }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# メインループから呼ぶ。表示を更新すべき変化があれば true
|
|
117
|
+
def poll
|
|
118
|
+
finished, changed = @mutex.synchronize do
|
|
119
|
+
dirs = @finished_dirs
|
|
120
|
+
@finished_dirs = []
|
|
121
|
+
progress = @progress_changed
|
|
122
|
+
@progress_changed = false
|
|
123
|
+
[dirs, progress || !dirs.empty?]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
current = @directory_listing&.current_path
|
|
127
|
+
@directory_listing.refresh if current && finished.any? { |dir| File.expand_path(dir) == File.expand_path(current) }
|
|
128
|
+
changed
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def confirm_and_extract(plan)
|
|
134
|
+
lines = ['', "Extract #{plan.archives.length} archive(s)?", '']
|
|
135
|
+
plan.archives.first(5).each do |zip, dest|
|
|
136
|
+
lines << " #{TextUtils.shorten_path_from_end(File.basename(zip), 20)} -> #{TextUtils.shorten_path_from_end(File.basename(dest), 20)}/"
|
|
137
|
+
end
|
|
138
|
+
lines << " ... #{plan.archives.length - 5} more" if plan.archives.length > 5
|
|
139
|
+
lines += ['', "To: #{TextUtils.shorten_path_from_end(plan.dest_dir, 40)}", '', ' [Y]es - Extract', ' [N]o - Cancel', '']
|
|
140
|
+
|
|
141
|
+
return false unless confirm_dialog('Extract Archives', lines, dialog_options(lines))
|
|
142
|
+
|
|
143
|
+
plan.archives.each { |zip, dest| start_extract(zip, dest) }
|
|
144
|
+
true
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def prompt_and_compress(plan)
|
|
148
|
+
input = @dialog_renderer.show_input_dialog(
|
|
149
|
+
"Compress #{plan.items.length} item(s)",
|
|
150
|
+
"Archive name (Enter: #{plan.default_name}):",
|
|
151
|
+
{ width: DIALOG_WIDTH, border_color: "\e[36m", title_color: "\e[1;36m", content_color: "\e[37m" }
|
|
152
|
+
)
|
|
153
|
+
return false if input.nil?
|
|
154
|
+
|
|
155
|
+
name = self.class.archive_name(input, plan.default_name)
|
|
156
|
+
zip_path = name && File.join(plan.dest_dir, name)
|
|
157
|
+
if zip_path.nil? || File.exist?(zip_path)
|
|
158
|
+
message = zip_path ? "Already exists: #{name}" : "Invalid archive name: #{input}"
|
|
159
|
+
@notification_manager&.add(message, :error, duration: 3, exit_code: nil)
|
|
160
|
+
return false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
start_compress(zip_path, plan.source_dir, plan.items)
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def dialog_options(lines)
|
|
168
|
+
{
|
|
169
|
+
width: DIALOG_WIDTH,
|
|
170
|
+
height: DIALOG_BORDER_HEIGHT + lines.length,
|
|
171
|
+
border_color: "\e[36m",
|
|
172
|
+
title_color: "\e[1;36m",
|
|
173
|
+
content_color: "\e[37m"
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# refresh_dirs: 完了時に表示中なら再読み込みするディレクトリ
|
|
178
|
+
def run_job(name, refresh_dirs)
|
|
179
|
+
job = @job_manager.add_job(name: name, path: refresh_dirs.first, command: nil)
|
|
180
|
+
job.start
|
|
181
|
+
last_percent = -PROGRESS_STEP
|
|
182
|
+
progress = lambda do |done, total|
|
|
183
|
+
percent = total.zero? ? 100 : (done * 100 / total)
|
|
184
|
+
next if percent < last_percent + PROGRESS_STEP
|
|
185
|
+
|
|
186
|
+
last_percent = percent
|
|
187
|
+
job.name = "#{name} #{percent}%"
|
|
188
|
+
@mutex.synchronize { @progress_changed = true }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
Thread.new do
|
|
192
|
+
begin
|
|
193
|
+
yield job, progress
|
|
194
|
+
job.name = name
|
|
195
|
+
job.complete(exit_code: 0) unless job.cancelled?
|
|
196
|
+
rescue ZipExtractor::CancelledError, ZipWriter::CancelledError
|
|
197
|
+
job.name = name
|
|
198
|
+
job.append_log('Cancelled')
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
job.name = name
|
|
201
|
+
job.append_log("Error: #{e.message}")
|
|
202
|
+
job.fail(exit_code: 1) unless job.cancelled?
|
|
203
|
+
ensure
|
|
204
|
+
@job_manager.notify_completion(job) unless job.cancelled?
|
|
205
|
+
@mutex.synchronize { @finished_dirs.concat(refresh_dirs) }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
job
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -135,6 +135,15 @@ module Rufio
|
|
|
135
135
|
@completion_message
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
# 完了メッセージを一度だけ受け取る。実行中は消費しない。
|
|
139
|
+
def consume_completion_message
|
|
140
|
+
return nil if running?
|
|
141
|
+
|
|
142
|
+
message = @completion_message
|
|
143
|
+
@completion_message = nil
|
|
144
|
+
message
|
|
145
|
+
end
|
|
146
|
+
|
|
138
147
|
# 現在実行中のコマンド名を取得
|
|
139
148
|
# @return [String, nil] コマンド名(実行中でない場合はnil)
|
|
140
149
|
def current_command
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rufio
|
|
4
|
+
# 完了通知とフッターランプで共有する実行状態・表示期限。
|
|
5
|
+
# 描画側は message を読むだけにし、状態の更新はメインループで行う。
|
|
6
|
+
class BackgroundStatus
|
|
7
|
+
POLL_INTERVAL = 0.1
|
|
8
|
+
DISPLAY_DURATION = 3.0
|
|
9
|
+
|
|
10
|
+
attr_reader :message
|
|
11
|
+
|
|
12
|
+
def initialize(now: Time.now)
|
|
13
|
+
@last_poll = now
|
|
14
|
+
@running = false
|
|
15
|
+
@message = nil
|
|
16
|
+
@expires_at = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# 描画に関わる状態が変化した場合だけ true を返す。
|
|
20
|
+
def update(executor, now: Time.now)
|
|
21
|
+
changed = expire(now)
|
|
22
|
+
running = executor ? executor.running? : false
|
|
23
|
+
if running != @running
|
|
24
|
+
@running = running
|
|
25
|
+
changed = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if executor && now - @last_poll > POLL_INTERVAL
|
|
29
|
+
@last_poll = now
|
|
30
|
+
if (message = executor.consume_completion_message)
|
|
31
|
+
@message = message
|
|
32
|
+
@expires_at = now + DISPLAY_DURATION
|
|
33
|
+
changed = true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
changed
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def expire(now)
|
|
42
|
+
return false unless @expires_at && now >= @expires_at
|
|
43
|
+
|
|
44
|
+
@message = nil
|
|
45
|
+
@expires_at = nil
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|