rfd 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c8531a85da40abf299418a5db10c44a8df36510c
4
+ data.tar.gz: 20dd4b8c0560d21f506a41ad64aeec7fbacc0c35
5
+ SHA512:
6
+ metadata.gz: 6a045b387ff889e51b0e0af604f6248849f0e160bff2b41d5ee4da3b3db4de93d35422f84aa1dbdf0fc979db08cc2ce9c68bdd9f345c7caa90c3cf524e18b119
7
+ data.tar.gz: 4b4701049e945298a437cdf42616f71e3b7ad7b556c6c244c16e8d38cab2c8957d6e8c5538799853bf58f56fa48b28c2ee424565059264ba0c0b8be442fb6ad9
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rfd.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Akira Matsuda
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,31 @@
1
+ # rfd (Ruby on Files and Directories)
2
+
3
+ rfd is a terminal based File explorer, inpsired by the legendary freesoft MS-DOS filer, "FD".
4
+
5
+ ## Installation
6
+
7
+ % gem install rfd
8
+
9
+ ## Requirements
10
+
11
+ * Ruby 2.0, Ruby 2.1
12
+ * NCurses
13
+ * (FFI)
14
+
15
+ ## Tested environment
16
+
17
+ Max OSX Snow Leopard
18
+
19
+ ## Usage
20
+
21
+ Open up your terminal and type in:
22
+
23
+ % rfd
24
+
25
+ You can command rfd by pressing some chars on your keyboard, just like Vim.
26
+
27
+ All available commands are defined here. https://github.com/amatsuda/rfd/tree/master/lib/rfd/commands.rb
28
+
29
+ ## Contributing
30
+
31
+ Send me your pull requests here. https://github.com/amatsuda/rfd
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/rfd ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path('../../lib/rfd', __FILE__)
3
+
4
+ rfd = Rfd.start ARGV[0] || '.'
5
+ rfd.run
@@ -0,0 +1,456 @@
1
+ require 'ffi-ncurses'
2
+ Curses = FFI::NCurses
3
+ require 'fileutils'
4
+ require_relative 'rfd/commands'
5
+ require_relative 'rfd/item'
6
+ require_relative 'rfd/windows'
7
+
8
+ module Rfd
9
+ VERSION = Gem.loaded_specs['rfd'].version.to_s
10
+
11
+ # :nodoc:
12
+ def self.init_curses
13
+ Curses.initscr
14
+ Curses.raw
15
+ Curses.noecho
16
+ Curses.curs_set 0
17
+ Curses.keypad Curses.stdscr, true
18
+ Curses.start_color
19
+
20
+ [Curses::COLOR_WHITE, Curses::COLOR_CYAN, Curses::COLOR_MAGENTA, Curses::COLOR_GREEN, Curses::COLOR_RED].each do |c|
21
+ Curses.init_pair c, c, Curses::COLOR_BLACK
22
+ end
23
+ end
24
+
25
+ # Start the app here!
26
+ #
27
+ # ==== Parameters
28
+ # * +dir+ - The initial directory.
29
+ def self.start(dir = '.')
30
+ init_curses
31
+ rfd = Rfd::Controller.new
32
+ rfd.cd dir
33
+ rfd.ls
34
+ rfd
35
+ end
36
+
37
+ class Controller
38
+ include Rfd::Commands
39
+
40
+ attr_reader :header_l, :header_r, :main, :command_line, :items, :displayed_items, :current_row, :current_page, :current_dir
41
+
42
+ # :nodoc:
43
+ def initialize
44
+ @main = MainWindow.new
45
+ @header_l = HeaderLeftWindow.new
46
+ @header_r = HeaderRightWindow.new
47
+ @command_line = CommandLineWindow.new
48
+ end
49
+
50
+ # The main loop.
51
+ def run
52
+ loop do
53
+ begin
54
+ case (c = Curses.getch)
55
+ when Curses::KEY_RETURN
56
+ enter
57
+ when Curses::KEY_ESCAPE
58
+ q
59
+ when 32 # space
60
+ space
61
+ when 127 # DEL
62
+ del
63
+ when Curses::KEY_DOWN
64
+ j
65
+ when Curses::KEY_UP
66
+ k
67
+ when Curses::KEY_LEFT
68
+ h
69
+ when Curses::KEY_RIGHT
70
+ l
71
+ when Curses::KEY_CTRL_A..Curses::KEY_CTRL_Z
72
+ chr = ((c - 1 + 65) ^ 0b0100000).chr
73
+ public_send "ctrl_#{chr}" if respond_to?("ctrl_#{chr}")
74
+ when 0..255
75
+ if respond_to? c.chr
76
+ public_send c.chr
77
+ else
78
+ debug "key: #{c}" if ENV['DEBUG']
79
+ end
80
+ else
81
+ debug "key: #{c}" if ENV['DEBUG']
82
+ end
83
+ rescue StopIteration
84
+ raise
85
+ rescue => e
86
+ command_line.show_error e.to_s
87
+ raise if ENV['DEBUG']
88
+ end
89
+ end
90
+ ensure
91
+ Curses.endwin
92
+ end
93
+
94
+ # Change the number of columns in the main window.
95
+ def spawn_panes(num)
96
+ main.spawn_panes num
97
+ @current_row = @current_page = 0
98
+ end
99
+
100
+ # The file or directory on which the cursor is on.
101
+ def current_item
102
+ items[current_row]
103
+ end
104
+
105
+ # * marked files and directories.
106
+ def marked_items
107
+ items.select(&:marked?)
108
+ end
109
+
110
+ # Marked files and directories or Array(the current file or directory).
111
+ #
112
+ # . and .. will not be included.
113
+ def selected_items
114
+ ((m = marked_items).any? ? m : Array(current_item)).reject {|i| %w(. ..).include? i.name}
115
+ end
116
+
117
+ # Move the cursor to specified row.
118
+ #
119
+ # The main window and the headers will be updated reflecting the displayed files and directories.
120
+ # The row number can be out of range of the current page.
121
+ def move_cursor(row = nil)
122
+ if row
123
+ page, item_index_in_page = row.divmod max_items
124
+ if page != current_page
125
+ switch_page page
126
+ else
127
+ if (prev_item = items[current_row])
128
+ main.draw_item prev_item
129
+ end
130
+ end
131
+ main.activate_pane item_index_in_page / maxy
132
+ @current_row = row
133
+ else
134
+ @current_row = 0
135
+ end
136
+
137
+ item = items[current_row]
138
+ main.draw_item item, current: true
139
+
140
+ header_l.draw_current_file_info item
141
+ header_l.wrefresh
142
+ end
143
+
144
+ # Change the current directory.
145
+ def cd(dir, pushd: true)
146
+ target = File.expand_path(dir.is_a?(Rfd::Item) ? dir.path : dir.start_with?('/') ? dir : current_dir ? File.join(current_dir, dir) : dir)
147
+ if File.readable? target
148
+ Dir.chdir target
149
+ (@dir_history ||= []) << current_dir if current_dir && pushd
150
+ @current_dir, @current_page, @current_row = target, 0, nil
151
+ main.activate_pane 0
152
+ end
153
+ end
154
+
155
+ # cd to the previous directory.
156
+ def popd
157
+ if defined?(@dir_history) && @dir_history.any?
158
+ cd @dir_history.pop, pushd: false
159
+ ls
160
+ end
161
+ end
162
+
163
+ # Fetch files from current directory.
164
+ # Then update each windows reflecting the newest information.
165
+ def ls
166
+ fetch_items_from_filesystem
167
+ sort_items_according_to_current_direction
168
+
169
+ @current_page ||= 0
170
+ draw_items
171
+ move_cursor current_row
172
+
173
+ draw_marked_items
174
+ draw_total_items
175
+ end
176
+
177
+ # Sort the whole files and directories in the current directory, then refresh the screen.
178
+ #
179
+ # ==== Parameters
180
+ # * +direction+ - Sort order in a String.
181
+ # nil : order by name
182
+ # r : reverse order by name
183
+ # s, S : order by file size
184
+ # sr, Sr: reverse order by file size
185
+ # t : order by mtime
186
+ # tr : reverse order by mtime
187
+ # c : order by ctime
188
+ # cr : reverse order by ctime
189
+ # u : order by atime
190
+ # ur : reverse order by atime
191
+ # e : order by extname
192
+ # er : reverse order by extname
193
+ def sort(direction = nil)
194
+ @direction, @current_page = direction, 0
195
+ sort_items_according_to_current_direction
196
+ switch_page 0
197
+ move_cursor 0
198
+ end
199
+
200
+ # Change the file permission of the selected files and directories.
201
+ #
202
+ # ==== Parameters
203
+ # * +mode+ - Unix chmod string (e.g. +w, g-r)
204
+ #
205
+ #TODO: accept number forms such as 755, 0644
206
+ def chmod(mode = nil)
207
+ return unless mode
208
+ FileUtils.chmod mode, selected_items.map(&:path)
209
+ ls
210
+ end
211
+
212
+ # Fetch files from current directory.
213
+ def fetch_items_from_filesystem
214
+ @items = Dir.foreach(current_dir).map {|fn| Item.new dir: current_dir, name: fn, window_width: maxx}.to_a
215
+ end
216
+
217
+ # Focus at the first file or directory of which name starts with the given String.
218
+ def find(str)
219
+ index = items.index {|i| i.name.start_with? str}
220
+ move_cursor index if index
221
+ end
222
+
223
+ # Focus at the last file or directory of which name starts with the given String.
224
+ def find_reverse(str)
225
+ index = items.reverse.index {|i| i.name.start_with? str}
226
+ move_cursor items.length - index - 1 if index
227
+ end
228
+
229
+ # Width of the currently active pane.
230
+ def maxx
231
+ main.maxx
232
+ end
233
+
234
+ # Height of the currently active pane.
235
+ def maxy
236
+ main.maxy
237
+ end
238
+
239
+ # Number of files or directories that the current main window can show in a page.
240
+ def max_items
241
+ main.max_items
242
+ end
243
+
244
+ # Update the main window with the loaded files and directories. Also update the header.
245
+ def draw_items
246
+ main.draw_items_to_each_pane (@displayed_items = items[current_page * max_items, max_items])
247
+ header_l.draw_path_and_page_number path: current_dir, current: current_page + 1, total: total_pages
248
+ end
249
+
250
+ # Sort the loaded files and directories in already given sort order.
251
+ def sort_items_according_to_current_direction
252
+ case @direction
253
+ when nil
254
+ @items = items.shift(2) + items.partition(&:directory?).flat_map(&:sort)
255
+ when 'r'
256
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort.reverse}
257
+ when 'S', 's'
258
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort_by {|i| -i.size}}
259
+ when 'Sr', 'sr'
260
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort_by(&:size)}
261
+ when 't'
262
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort {|x, y| y.mtime <=> x.mtime}}
263
+ when 'tr'
264
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort_by(&:mtime)}
265
+ when 'c'
266
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort {|x, y| y.ctime <=> x.ctime}}
267
+ when 'cr'
268
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort_by(&:ctime)}
269
+ when 'u'
270
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort {|x, y| y.atime <=> x.atime}}
271
+ when 'ur'
272
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort_by(&:atime)}
273
+ when 'e'
274
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort {|x, y| y.extname <=> x.extname}}
275
+ when 'er'
276
+ @items = items.shift(2) + items.partition(&:directory?).flat_map {|arr| arr.sort_by(&:extname)}
277
+ end
278
+ items.each.with_index {|item, index| item.index = index}
279
+ end
280
+
281
+ # Search files and directories from the current directory, and update the screen.
282
+ #
283
+ # * +pattern+ - Search pattern against file names in Ruby Regexp string.
284
+ #
285
+ # === Example
286
+ #
287
+ # a : Search files that contains the letter "a" in their file name
288
+ # .*\.pdf$ : Search PDF files
289
+ def grep(pattern = '.*')
290
+ regexp = Regexp.new(pattern)
291
+ fetch_items_from_filesystem
292
+ @items = items.shift(2) + items.select {|i| i.name =~ regexp}
293
+ sort_items_according_to_current_direction
294
+ switch_page 0
295
+ move_cursor 0
296
+
297
+ draw_total_items
298
+ end
299
+
300
+ # Copy selected files and directories to the destination.
301
+ def cp(dest)
302
+ src = (m = marked_items).any? ? m.map(&:path) : current_item.path
303
+ FileUtils.cp_r src, File.join(current_dir, dest)
304
+ ls
305
+ end
306
+
307
+ # Move selected files and directories to the destination.
308
+ def mv(dest)
309
+ src = (m = marked_items).any? ? m.map(&:path) : current_item.path
310
+ FileUtils.mv src, File.join(current_dir, dest)
311
+ ls
312
+ end
313
+
314
+ # Rename selected files and directories.
315
+ #
316
+ # ==== Parameters
317
+ # * +pattern+ - / separated Regexp like string
318
+ def rename(pattern)
319
+ from, to = pattern.split '/'
320
+ from = Regexp.new from
321
+ selected_items.each do |item|
322
+ name = item.name.gsub from, to
323
+ FileUtils.mv item.path, File.join(current_dir, name)
324
+ end
325
+ ls
326
+ end
327
+
328
+ # Create a new directory.
329
+ def mkdir(dir)
330
+ FileUtils.mkdir_p File.join(current_dir, dir)
331
+ ls
332
+ end
333
+
334
+ # Create a new empty file.
335
+ def touch(filename)
336
+ FileUtils.touch File.join(current_dir, filename)
337
+ ls
338
+ end
339
+
340
+ # Current page is the first page?
341
+ def first_page?
342
+ current_page == 0
343
+ end
344
+
345
+ # Do we have more pages?
346
+ def last_page?
347
+ current_page == total_pages - 1
348
+ end
349
+
350
+ # Number of pages in the current directory.
351
+ def total_pages
352
+ items.length / max_items + 1
353
+ end
354
+
355
+ # Move to the given page number.
356
+ #
357
+ # ==== Parameters
358
+ # * +page+ - Target page number
359
+ def switch_page(page)
360
+ @current_page = page
361
+ draw_items
362
+ end
363
+
364
+ # Update the header information concerning currently marked files or directories.
365
+ def draw_marked_items
366
+ items = marked_items
367
+ header_r.draw_marked_items count: items.size, size: items.inject(0) {|sum, i| sum += i.size}
368
+ end
369
+
370
+ # Update the header information concerning total files and directories in the current directory.
371
+ def draw_total_items
372
+ header_r.draw_total_items count: items.size, size: items.inject(0) {|sum, i| sum += i.size}
373
+ end
374
+
375
+ def toggle_mark
376
+ main.toggle_mark current_item
377
+ end
378
+
379
+ # Accept user input, and directly execute it as a Ruby method call to the controller.
380
+ #
381
+ # ==== Parameters
382
+ # * +preset_command+ - A command that would be displayed at the command line before user input.
383
+ def process_command_line(preset_command: nil)
384
+ prompt = preset_command ? ":#{preset_command} " : ':'
385
+ command_line.set_prompt prompt
386
+ cmd, *args = command_line.get_command(prompt: prompt).split(' ')
387
+ if cmd && !cmd.empty? && respond_to?(cmd)
388
+ self.public_send cmd, *args
389
+ command_line.wclear
390
+ command_line.wrefresh
391
+ end
392
+ rescue Interrupt
393
+ command_line.wclear
394
+ command_line.wrefresh
395
+ end
396
+
397
+ # Accept user input, and directly execute it in an external shell.
398
+ def process_shell_command
399
+ command_line.set_prompt ':!'
400
+ cmd = command_line.get_command(prompt: ':!')[1..-1]
401
+ execute_external_command pause: true do
402
+ system cmd
403
+ end
404
+ rescue Interrupt
405
+ ensure
406
+ command_line.wclear
407
+ command_line.wrefresh
408
+ end
409
+
410
+ # Let the user answer y or n.
411
+ #
412
+ # ==== Parameters
413
+ # * +prompt+ - Prompt message
414
+ def ask(prompt = '(y/n)')
415
+ command_line.set_prompt prompt
416
+ command_line.wrefresh
417
+ while (c = Curses.getch)
418
+ next unless [78, 89, 110, 121, 3, 27] .include? c # N, Y, n, y, ^c, esc
419
+ command_line.wclear
420
+ command_line.wrefresh
421
+ break [89, 121].include? c # Y, y
422
+ end
423
+ end
424
+
425
+ # Open current file or directory with the editor.
426
+ def edit
427
+ execute_external_command do
428
+ editor = ENV['EDITOR'] || 'vim'
429
+ system %Q[#{editor} "#{current_item.path}"]
430
+ end
431
+ end
432
+
433
+ # Open current file or directory with the viewer.
434
+ def view
435
+ execute_external_command do
436
+ pager = ENV['PAGER'] || 'less'
437
+ system %Q[#{pager} "#{current_item.path}"]
438
+ end
439
+ end
440
+
441
+ private
442
+ def execute_external_command(pause: false)
443
+ Curses.def_prog_mode
444
+ Curses.endwin
445
+ yield
446
+ ensure
447
+ Curses.reset_prog_mode
448
+ Curses.getch if pause
449
+ Curses.refresh
450
+ end
451
+
452
+ def debug(str)
453
+ header_r.debug str
454
+ end
455
+ end
456
+ end