rfd 0.7.1 → 0.8.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/.github/workflows/test.yml +37 -0
- data/.gitignore +2 -0
- data/Gemfile +2 -0
- data/README.md +71 -9
- data/Rakefile +13 -1
- data/lib/rfd/bookmark.rb +58 -0
- data/lib/rfd/bookmark_window.rb +141 -0
- data/lib/rfd/commands.rb +343 -239
- data/lib/rfd/controller.rb +423 -0
- data/lib/rfd/file_ops.rb +489 -0
- data/lib/rfd/filter_input.rb +80 -0
- data/lib/rfd/help.txt +46 -0
- data/lib/rfd/help_generator.rb +88 -0
- data/lib/rfd/item.rb +174 -15
- data/lib/rfd/logging.rb +1 -1
- data/lib/rfd/navigation_window.rb +432 -0
- data/lib/rfd/preview/client.rb +155 -0
- data/lib/rfd/preview/request.rb +40 -0
- data/lib/rfd/preview/result.rb +70 -0
- data/lib/rfd/preview/server.rb +394 -0
- data/lib/rfd/preview_window.rb +308 -0
- data/lib/rfd/reline_ext.rb +16 -0
- data/lib/rfd/sub_window.rb +85 -0
- data/lib/rfd/viewer.rb +398 -0
- data/lib/rfd/windows.rb +39 -34
- data/lib/rfd.rb +24 -739
- data/rfd.gemspec +7 -2
- metadata +62 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 583342396eea5d2103dd2deb3bf55cbd46be8349fd339f8a60709231bdc66d81
|
|
4
|
+
data.tar.gz: e5e8d3be8efdbdd392587532ccc501c1f714ddac1ee639aa93e7b6c3a1e24ad0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f60a2c8684d96b23caae0b68a67ed19e24f60817d87095cc7bd316f56416029f653d18269f9b28703b86fd00be29311bb8cd5e261cb8348a4b7cf5a78b6208d5
|
|
7
|
+
data.tar.gz: 7b9a8d13d1176548740265bf4313d29c8b2aaa03b28be7fb933afeb42ad2380c0357a4a97ec8bcf18aaf669025490bb23f7d235a0fe355a3a03b44d35bcec4bc
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest]
|
|
16
|
+
ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v6
|
|
20
|
+
|
|
21
|
+
- name: Set up Ruby
|
|
22
|
+
uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: ${{ matrix.ruby }}
|
|
25
|
+
bundler-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Install ncurses (Ubuntu)
|
|
28
|
+
if: runner.os == 'Linux'
|
|
29
|
+
run: sudo apt-get install -y libncursesw5-dev
|
|
30
|
+
|
|
31
|
+
- name: Set test environment
|
|
32
|
+
run: |
|
|
33
|
+
echo "TERM=xterm-256color" >> $GITHUB_ENV
|
|
34
|
+
echo "RFD_SKIP_PREVIEW_SERVER=1" >> $GITHUB_ENV
|
|
35
|
+
|
|
36
|
+
- name: Run tests
|
|
37
|
+
run: bundle exec rspec
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# rfd (Ruby on Files & Directories)
|
|
2
2
|
|
|
3
|
-
rfd is a terminal-based filesystem explorer, inspired by the legendary freesoft MS-DOS filer
|
|
3
|
+
rfd is a terminal-based filesystem explorer, inspired by the legendary freesoft MS-DOS filer "FD", with Vim flavor.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,12 +8,20 @@ rfd is a terminal-based filesystem explorer, inspired by the legendary freesoft
|
|
|
8
8
|
|
|
9
9
|
## Requirements
|
|
10
10
|
|
|
11
|
-
* Ruby 2.
|
|
12
|
-
* NCurses
|
|
11
|
+
* Ruby 2.5 or newer
|
|
12
|
+
* NCurses with wide character support (ncursesw) recommended for best Unicode display
|
|
13
|
+
|
|
14
|
+
### macOS: Setting up ncursesw for Unicode borders
|
|
15
|
+
|
|
16
|
+
macOS comes with an older system ncurses that doesn't fully support Unicode. For proper Unicode box-drawing characters, install and link Homebrew's ncurses before installing the curses gem:
|
|
17
|
+
|
|
18
|
+
% brew install ncurses
|
|
19
|
+
% brew link --force ncurses
|
|
20
|
+
% gem install curses
|
|
13
21
|
|
|
14
22
|
## Tested environments
|
|
15
23
|
|
|
16
|
-
Mac OS X Mountain Lion
|
|
24
|
+
Mac OS X Mountain Lion and newer, Ubuntu 13.04 and newer
|
|
17
25
|
|
|
18
26
|
## Screenshot
|
|
19
27
|
|
|
@@ -29,19 +37,23 @@ You can also pass in a starting directory name, which defaults to `.`.
|
|
|
29
37
|
|
|
30
38
|
% rfd ~/src/rails
|
|
31
39
|
|
|
40
|
+
|
|
32
41
|
## Commands
|
|
33
42
|
|
|
34
43
|
You can send commands to rfd by pressing some chars on your keyboard, just like Vim.
|
|
35
44
|
If you're unfamiliar with this sort of command system, I recommend you to play with `vimtutor` before you go any further.
|
|
36
45
|
|
|
46
|
+
Press `?` to see the built-in help screen with all available commands.
|
|
47
|
+
|
|
37
48
|
All available commands in rfd are defined as Ruby methods here. https://github.com/amatsuda/rfd/tree/master/lib/rfd/commands.rb
|
|
38
49
|
|
|
39
50
|
### Changing the current directory
|
|
40
51
|
|
|
41
|
-
* `<Enter>`: cd into the directory where the cursor is on.
|
|
52
|
+
* `<Enter>`: cd into the directory where the cursor is on. For files, opens with viewer.
|
|
42
53
|
* `<Delete>` (or \<Backspace\> on your keyboard, probably?): Go up to the upper directory (cd ..).
|
|
43
54
|
* `-`: Get back to where you once belonged (popd).
|
|
44
|
-
* `@`:
|
|
55
|
+
* `@`: Open directory tree browser for navigation.
|
|
56
|
+
* `~`: Go to home directory.
|
|
45
57
|
|
|
46
58
|
### Moving the cursor
|
|
47
59
|
|
|
@@ -75,6 +87,7 @@ You can find a file by typing the first letter of it immediately after the find
|
|
|
75
87
|
* `f{char}`: Move to the next file / directory of which name starts with the given char.
|
|
76
88
|
* `F{char}`: Move to the previous file / directory of which name starts with the given char.
|
|
77
89
|
* `n`: Repeat the last `f` or `F`.
|
|
90
|
+
* `N`: Repeat the last `f` or `F` in reverse direction.
|
|
78
91
|
|
|
79
92
|
### Searching, sorting
|
|
80
93
|
|
|
@@ -107,8 +120,8 @@ The mark is drawn as a `*` char on the left of each file / directory name.
|
|
|
107
120
|
|
|
108
121
|
As stated above, you can send a command to one or more files / directories. In this document, the term "selected items" means "(the marked files / directories) || (the file / directory on which the cursor is on)".
|
|
109
122
|
|
|
110
|
-
* `c`: Copy selected items (cp).
|
|
111
|
-
* `m`: Move selected items (mv).
|
|
123
|
+
* `c`: Copy selected items (cp). Opens tree browser to select destination.
|
|
124
|
+
* `m`: Move selected items (mv). Opens tree browser to select destination.
|
|
112
125
|
* `d`: Move selected items into the Trash.
|
|
113
126
|
* `D`: Delete selected items.
|
|
114
127
|
* `r`: Rename selected items. This command takes a sed-like argument separated by a `/`. For example, changing all .html files' extension to .html.erb could be done by `\.html$/.html.erb`.
|
|
@@ -133,10 +146,11 @@ As stated above, you can send a command to one or more files / directories. In t
|
|
|
133
146
|
|
|
134
147
|
### Viewing, Editing, Opening
|
|
135
148
|
|
|
136
|
-
* `<Enter>`:
|
|
149
|
+
* `<Enter>`: Open directory, or view file. For images, shows inline preview. For audio, plays with system player.
|
|
137
150
|
* `v`: View current file with the system $VIEWER such as `less`.
|
|
138
151
|
* `e`: Edit current file with the system $EDITOR such as `vim`.
|
|
139
152
|
* `o`: Send the `open` command.
|
|
153
|
+
* `P`: Toggle preview window (shows file contents with syntax highlighting).
|
|
140
154
|
|
|
141
155
|
### Manipulating archives
|
|
142
156
|
|
|
@@ -161,6 +175,7 @@ Mouse is available if your terminal supports it. You can move the cursor by clic
|
|
|
161
175
|
* `C`: Copy selected items' paths to the clipboard.
|
|
162
176
|
* `O`: Open a new terminal window at the current directory.
|
|
163
177
|
* `!`: Execute a shell command.
|
|
178
|
+
* `?`: Show help screen.
|
|
164
179
|
* `q`: Quit the app.
|
|
165
180
|
|
|
166
181
|
## How to manually execute a command, or how the commands are executed
|
|
@@ -168,6 +183,53 @@ Mouse is available if your terminal supports it. You can move the cursor by clic
|
|
|
168
183
|
By pressing `:`, you can enter the command-line mode. Any string given in the command line after `:` will be executed as Ruby method call in the `Controller` instance.
|
|
169
184
|
For instance, `:j` brings your cursor down, `:mkdir foo` makes a directory named "foo". And `:q!` of course works as you might expect, since `q!` method is implemented so.
|
|
170
185
|
|
|
186
|
+
|
|
187
|
+
## Features
|
|
188
|
+
|
|
189
|
+
### File Icons
|
|
190
|
+
|
|
191
|
+
rfd displays file type icons using Nerd Fonts. If you have a Nerd Font installed, you'll see icons for directories, symlinks, and various file types (Ruby, JavaScript, Markdown, etc.).
|
|
192
|
+
|
|
193
|
+
To disable icons:
|
|
194
|
+
|
|
195
|
+
% RFD_NO_ICONS=1 rfd
|
|
196
|
+
|
|
197
|
+
### Preview Window
|
|
198
|
+
|
|
199
|
+
When the file list is multi-paned (default), rfd shows the preview window in an inactive pane. The preview window shows a preview of the file or directory where the cursor is on.
|
|
200
|
+
|
|
201
|
+
* **Code files**: Syntax-highlighted preview (via Rouge)
|
|
202
|
+
* **Directories**: List of contents
|
|
203
|
+
* **Archives**: Tree view of zip/tar.gz contents
|
|
204
|
+
* **Markdown**: Formatted with headers and lists highlighted
|
|
205
|
+
* **Images**: Inline preview (in supported terminals)
|
|
206
|
+
* **Binary files**: Indicated as `[Binary file]`
|
|
207
|
+
|
|
208
|
+
However, with this feature enabled, the cursor would not move smoothly. In that case, you can disable (toggle) the preview window by pressing `P`.
|
|
209
|
+
|
|
210
|
+
### Directory Tree Browser
|
|
211
|
+
|
|
212
|
+
Press `@` to open an interactive directory tree browser with:
|
|
213
|
+
* **Fuzzy filtering**: Type to filter directories
|
|
214
|
+
* **Tree navigation**: `j/k` or arrows to move, `Enter` to select
|
|
215
|
+
* **Expand/collapse**: `h/l` to collapse/expand directories
|
|
216
|
+
* **Quick access**: `~` for home, `/` for root
|
|
217
|
+
* **Bookmarks**: `@` to switch to bookmark view
|
|
218
|
+
|
|
219
|
+
### Bookmarks
|
|
220
|
+
|
|
221
|
+
Save frequently used directories for quick access:
|
|
222
|
+
* In tree or bookmark view, press `^B` to toggle bookmark on current directory
|
|
223
|
+
* Press `@` twice (or `@` then `@` in tree view) to see bookmarks
|
|
224
|
+
* Bookmarks are saved to `~/.config/rfd/bookmarks`
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
## Environment Variables
|
|
228
|
+
|
|
229
|
+
* `RFD_NO_ICONS`: Set to `1` to disable file icons (useful if you don't have a Nerd Font installed)
|
|
230
|
+
* `EDITOR`: Editor used for the `e` command (default: vim)
|
|
231
|
+
* `VIEWER`: Viewer used for the `v` command (default: less)
|
|
232
|
+
|
|
171
233
|
## Contributing
|
|
172
234
|
|
|
173
235
|
Send me your pull requests here. https://github.com/amatsuda/rfd
|
data/Rakefile
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'bundler'
|
|
3
3
|
Bundler::GemHelper.install_tasks
|
|
4
|
-
require
|
|
4
|
+
require 'bundler/gem_tasks'
|
|
5
5
|
|
|
6
6
|
require 'rspec/core'
|
|
7
7
|
require 'rspec/core/rake_task'
|
|
@@ -11,3 +11,15 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
task :default => :spec
|
|
14
|
+
|
|
15
|
+
namespace :build do
|
|
16
|
+
desc 'Generate help text cache'
|
|
17
|
+
task :help do
|
|
18
|
+
require_relative 'lib/rfd/help_generator'
|
|
19
|
+
Rfd::HelpGenerator.write_cache
|
|
20
|
+
puts "Generated #{Rfd::HelpGenerator::CACHE_FILE}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Generate help before building the gem
|
|
25
|
+
Rake::Task[:build].enhance(['build:help'])
|
data/lib/rfd/bookmark.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rfd
|
|
4
|
+
module Bookmark
|
|
5
|
+
CONFIG_DIR = File.join(ENV.fetch('XDG_CONFIG_HOME') { File.expand_path('~/.config') }, 'rfd')
|
|
6
|
+
BOOKMARK_FILE = File.join(CONFIG_DIR, 'bookmarks')
|
|
7
|
+
|
|
8
|
+
@bookmarks = []
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_accessor :bookmarks
|
|
12
|
+
|
|
13
|
+
def add(path)
|
|
14
|
+
path = File.expand_path(path)
|
|
15
|
+
return if @bookmarks.include?(path)
|
|
16
|
+
|
|
17
|
+
@bookmarks << path
|
|
18
|
+
save
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def remove(path)
|
|
22
|
+
path = File.expand_path(path)
|
|
23
|
+
@bookmarks.delete(path)
|
|
24
|
+
save
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def include?(path)
|
|
28
|
+
@bookmarks.include?(File.expand_path(path))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def toggle(path)
|
|
32
|
+
if include?(path)
|
|
33
|
+
remove(path)
|
|
34
|
+
else
|
|
35
|
+
add(path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def load
|
|
40
|
+
return unless File.exist?(BOOKMARK_FILE)
|
|
41
|
+
|
|
42
|
+
@bookmarks = File.readlines(BOOKMARK_FILE, chomp: true)
|
|
43
|
+
.map { |line| File.expand_path(line) }
|
|
44
|
+
.select { |path| File.directory?(path) }
|
|
45
|
+
rescue Errno::EACCES, Errno::ENOENT
|
|
46
|
+
@bookmarks = []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def save
|
|
50
|
+
dir = File.dirname(BOOKMARK_FILE)
|
|
51
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
52
|
+
File.write(BOOKMARK_FILE, @bookmarks.join("\n") + "\n")
|
|
53
|
+
rescue Errno::EACCES, Errno::ENOENT
|
|
54
|
+
# Silently fail if we can't write
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rfd
|
|
4
|
+
class BookmarkWindow < SubWindow
|
|
5
|
+
def initialize(controller)
|
|
6
|
+
super(controller)
|
|
7
|
+
@cursor = 0
|
|
8
|
+
@scroll = 0
|
|
9
|
+
@filter = FilterInput.new { apply_filter }
|
|
10
|
+
@filtered_items = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def all_items
|
|
14
|
+
Bookmark.bookmarks
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def display_items
|
|
18
|
+
@filtered_items || all_items
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def current_bookmarked?
|
|
22
|
+
Bookmark.include?(controller.current_dir.path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def visible_items
|
|
26
|
+
display_items[@scroll, max_height - 1] || []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def current_item
|
|
30
|
+
display_items[@cursor]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render
|
|
34
|
+
reposition_if_needed
|
|
35
|
+
@window.clear
|
|
36
|
+
|
|
37
|
+
draw_border('Bookmarks (@:tree ^B:add/remove ESC:close)')
|
|
38
|
+
|
|
39
|
+
# Filter input line (row 1)
|
|
40
|
+
@filter.render(@window, 1, max_width)
|
|
41
|
+
|
|
42
|
+
# Bookmarks list starts at row 2
|
|
43
|
+
visible_items.each_with_index do |path, i|
|
|
44
|
+
actual_index = @scroll + i
|
|
45
|
+
@window.setpos(2 + i, 1)
|
|
46
|
+
|
|
47
|
+
display_path = path.sub(File.expand_path('~'), '~')
|
|
48
|
+
if actual_index == @cursor
|
|
49
|
+
@window.attron(Curses::A_REVERSE) { @window.addstr(display_path[0, max_width].ljust(max_width)) }
|
|
50
|
+
else
|
|
51
|
+
@window.addstr(display_path[0, max_width].ljust(max_width))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@window.refresh
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def handle_input(c)
|
|
59
|
+
case c
|
|
60
|
+
when 27 # ESC - close window
|
|
61
|
+
controller.close_sub_window
|
|
62
|
+
true
|
|
63
|
+
when 64, ?@ # @ - switch to tree view
|
|
64
|
+
controller.close_sub_window
|
|
65
|
+
controller.instance_variable_set(:@sub_window, NavigationWindow.new(controller))
|
|
66
|
+
controller.instance_variable_get(:@sub_window).render
|
|
67
|
+
true
|
|
68
|
+
when 2 # Ctrl-B - toggle bookmark for current directory
|
|
69
|
+
toggle_bookmark
|
|
70
|
+
true
|
|
71
|
+
when 10, 13 # Enter - cd to selected bookmark
|
|
72
|
+
select_item
|
|
73
|
+
true
|
|
74
|
+
when 14 # Ctrl-N
|
|
75
|
+
move_cursor_down
|
|
76
|
+
true
|
|
77
|
+
when 16 # Ctrl-P
|
|
78
|
+
move_cursor_up
|
|
79
|
+
true
|
|
80
|
+
else
|
|
81
|
+
if @filter.handle_input(c)
|
|
82
|
+
render
|
|
83
|
+
true
|
|
84
|
+
else
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def apply_filter
|
|
93
|
+
if @filter.empty?
|
|
94
|
+
@filtered_items = nil
|
|
95
|
+
else
|
|
96
|
+
@filtered_items = all_items.select { |path| @filter.fuzzy_match?(path) }
|
|
97
|
+
end
|
|
98
|
+
@cursor = 0
|
|
99
|
+
@scroll = 0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def toggle_bookmark
|
|
103
|
+
Bookmark.toggle(controller.current_dir.path)
|
|
104
|
+
apply_filter # Re-apply filter in case list changed
|
|
105
|
+
render
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def select_item
|
|
109
|
+
item = current_item
|
|
110
|
+
return unless item
|
|
111
|
+
|
|
112
|
+
controller.close_sub_window
|
|
113
|
+
controller.cd(item)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def move_cursor_down
|
|
117
|
+
return if @cursor >= display_items.size - 1
|
|
118
|
+
|
|
119
|
+
@cursor += 1
|
|
120
|
+
adjust_scroll
|
|
121
|
+
render
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def move_cursor_up
|
|
125
|
+
return if @cursor <= 0
|
|
126
|
+
|
|
127
|
+
@cursor -= 1
|
|
128
|
+
adjust_scroll
|
|
129
|
+
render
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def adjust_scroll
|
|
133
|
+
available_height = max_height - 1 # Reserve one line for filter
|
|
134
|
+
if @cursor < @scroll
|
|
135
|
+
@scroll = @cursor
|
|
136
|
+
elsif @cursor >= @scroll + available_height
|
|
137
|
+
@scroll = @cursor - available_height + 1
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|