rfd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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