rfd 0.0.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c8531a85da40abf299418a5db10c44a8df36510c
4
- data.tar.gz: 20dd4b8c0560d21f506a41ad64aeec7fbacc0c35
3
+ metadata.gz: d1dd36a7cd7b7b8a56e25d704089e6c390935e07
4
+ data.tar.gz: eb3055ac47eb7a6105fb9a1a0b093323bab1d23a
5
5
  SHA512:
6
- metadata.gz: 6a045b387ff889e51b0e0af604f6248849f0e160bff2b41d5ee4da3b3db4de93d35422f84aa1dbdf0fc979db08cc2ce9c68bdd9f345c7caa90c3cf524e18b119
7
- data.tar.gz: 4b4701049e945298a437cdf42616f71e3b7ad7b556c6c244c16e8d38cab2c8957d6e8c5538799853bf58f56fa48b28c2ee424565059264ba0c0b8be442fb6ad9
6
+ metadata.gz: 14187a51b3c5d3ea90b7f4fcbd363170c9cd391e0d4ea7f54f75045de1a34e6dd762c56f61964af537b4a4e83d28325198e794c99c4dd942f8a94645d41a0a92
7
+ data.tar.gz: d1beb6cd74c3c03c06c02140d2d2680727ab27ac8cb2dfcb93da8659e1f0c23f3c879437701e6f81b8eb90884a298ac23786a166eb4f949c59faa403524fd5e7
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # rfd (Ruby on Files and Directories)
1
+ # rfd (Ruby on Files & Directories)
2
2
 
3
3
  rfd is a terminal based File explorer, inpsired by the legendary freesoft MS-DOS filer, "FD".
4
4
 
@@ -14,7 +14,7 @@ rfd is a terminal based File explorer, inpsired by the legendary freesoft MS-DOS
14
14
 
15
15
  ## Tested environment
16
16
 
17
- Max OSX Snow Leopard
17
+ Mac OSX Snow Leopard
18
18
 
19
19
  ## Usage
20
20
 
data/lib/rfd.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  require 'ffi-ncurses'
2
+ require 'ffi-ncurses/mouse'
2
3
  Curses = FFI::NCurses
3
4
  require 'fileutils'
5
+ require 'tmpdir'
6
+ require 'rubygems/package'
7
+ require 'zip'
8
+ require 'zip/filesystem'
4
9
  require_relative 'rfd/commands'
5
10
  require_relative 'rfd/item'
6
11
  require_relative 'rfd/windows'
@@ -20,6 +25,9 @@ module Rfd
20
25
  [Curses::COLOR_WHITE, Curses::COLOR_CYAN, Curses::COLOR_MAGENTA, Curses::COLOR_GREEN, Curses::COLOR_RED].each do |c|
21
26
  Curses.init_pair c, c, Curses::COLOR_BLACK
22
27
  end
28
+
29
+ Curses.mousemask Curses::ALL_MOUSE_EVENTS | Curses::REPORT_MOUSE_POSITION, nil
30
+ Curses.extend FFI::NCurses::Mouse
23
31
  end
24
32
 
25
33
  # Start the app here!
@@ -37,7 +45,7 @@ module Rfd
37
45
  class Controller
38
46
  include Rfd::Commands
39
47
 
40
- attr_reader :header_l, :header_r, :main, :command_line, :items, :displayed_items, :current_row, :current_page, :current_dir
48
+ attr_reader :header_l, :header_r, :main, :command_line, :items, :displayed_items, :current_row, :current_page, :current_dir, :current_zip
41
49
 
42
50
  # :nodoc:
43
51
  def initialize
@@ -45,10 +53,12 @@ module Rfd
45
53
  @header_l = HeaderLeftWindow.new
46
54
  @header_r = HeaderRightWindow.new
47
55
  @command_line = CommandLineWindow.new
56
+ @direction, @dir_history = nil, []
48
57
  end
49
58
 
50
59
  # The main loop.
51
60
  def run
61
+ mouse_event = Curses::MEVENT.new
52
62
  loop do
53
63
  begin
54
64
  case (c = Curses.getch)
@@ -77,6 +87,14 @@ module Rfd
77
87
  else
78
88
  debug "key: #{c}" if ENV['DEBUG']
79
89
  end
90
+ when Curses::KEY_MOUSE
91
+ if Curses.getmouse(mouse_event) == Curses::OK
92
+ if Curses.BUTTON_CLICK(mouse_event[:bstate], 1) > 0
93
+ click y: mouse_event[:y], x: mouse_event[:x]
94
+ elsif Curses.BUTTON_DOUBLE_CLICK(mouse_event[:bstate], 1) > 0
95
+ double_click y: mouse_event[:y], x: mouse_event[:x]
96
+ end
97
+ end
80
98
  else
81
99
  debug "key: #{c}" if ENV['DEBUG']
82
100
  end
@@ -143,18 +161,29 @@ module Rfd
143
161
 
144
162
  # Change the current directory.
145
163
  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
164
+ if dir.is_a?(Item) && dir.zip?
165
+ cd_into_zip dir
166
+ else
167
+ target = expand_path dir
168
+ if File.readable? target
169
+ Dir.chdir target
170
+ @dir_history << current_dir if current_dir && pushd
171
+ @current_dir, @current_page, @current_row, @current_zip = target, 0, nil, nil
172
+ main.activate_pane 0
173
+ end
152
174
  end
153
175
  end
154
176
 
177
+ def cd_into_zip(zipfile)
178
+ @current_zip = zipfile
179
+ @dir_history << current_dir if current_dir
180
+ @current_dir, @current_page, @current_row = zipfile.path, 0, nil
181
+ main.activate_pane 0
182
+ end
183
+
155
184
  # cd to the previous directory.
156
185
  def popd
157
- if defined?(@dir_history) && @dir_history.any?
186
+ if @dir_history.any?
158
187
  cd @dir_history.pop, pushd: false
159
188
  ls
160
189
  end
@@ -163,12 +192,12 @@ module Rfd
163
192
  # Fetch files from current directory.
164
193
  # Then update each windows reflecting the newest information.
165
194
  def ls
166
- fetch_items_from_filesystem
195
+ fetch_items_from_filesystem_or_zip
167
196
  sort_items_according_to_current_direction
168
197
 
169
198
  @current_page ||= 0
170
199
  draw_items
171
- move_cursor current_row
200
+ move_cursor (current_row ? [current_row, items.size - 1].min : nil)
172
201
 
173
202
  draw_marked_items
174
203
  draw_total_items
@@ -200,18 +229,46 @@ module Rfd
200
229
  # Change the file permission of the selected files and directories.
201
230
  #
202
231
  # ==== Parameters
203
- # * +mode+ - Unix chmod string (e.g. +w, g-r)
204
- #
205
- #TODO: accept number forms such as 755, 0644
232
+ # * +mode+ - Unix chmod string (e.g. +w, g-r, 755, 0644)
206
233
  def chmod(mode = nil)
207
234
  return unless mode
235
+ begin
236
+ Integer mode
237
+ mode = Integer mode.size == 3 ? "0#{mode}" : mode
238
+ rescue ArgumentError
239
+ end
208
240
  FileUtils.chmod mode, selected_items.map(&:path)
209
241
  ls
210
242
  end
211
243
 
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
244
+ # Change the file owner of the selected files and directories.
245
+ #
246
+ # ==== Parameters
247
+ # * +user_and_group+ - user name and group name separated by : (e.g. alice, nobody:nobody, :admin)
248
+ def chown(user_and_group)
249
+ return unless user_and_group
250
+ user, group = user_and_group.split(':').map {|s| s == '' ? nil : s}
251
+ FileUtils.chown user, group, selected_items.map(&:path)
252
+ ls
253
+ end
254
+
255
+ # Fetch files from current directory or current .zip file.
256
+ def fetch_items_from_filesystem_or_zip
257
+ unless in_zip?
258
+ @items = Dir.foreach(current_dir).map {|fn|
259
+ stat = File.lstat File.join(current_dir, fn)
260
+ Item.new dir: current_dir, name: fn, stat: stat, window_width: maxx
261
+ }.to_a
262
+ else
263
+ @items = [Item.new(dir: current_dir, name: '.', stat: File.stat(current_dir), window_width: maxx),
264
+ Item.new(dir: current_dir, name: '..', stat: File.stat(File.dirname(current_dir)), window_width: maxx)]
265
+ zf = Zip::File.new current_dir
266
+ zf.each {|entry|
267
+ next if entry.name_is_directory?
268
+ stat = zf.file.stat entry.name
269
+ @items << Item.new(dir: current_dir, name: entry.name, stat: stat, window_width: maxx)
270
+ }
271
+ end
215
272
  end
216
273
 
217
274
  # Focus at the first file or directory of which name starts with the given String.
@@ -223,7 +280,7 @@ module Rfd
223
280
  # Focus at the last file or directory of which name starts with the given String.
224
281
  def find_reverse(str)
225
282
  index = items.reverse.index {|i| i.name.start_with? str}
226
- move_cursor items.length - index - 1 if index
283
+ move_cursor items.size - index - 1 if index
227
284
  end
228
285
 
229
286
  # Width of the currently active pane.
@@ -288,7 +345,7 @@ module Rfd
288
345
  # .*\.pdf$ : Search PDF files
289
346
  def grep(pattern = '.*')
290
347
  regexp = Regexp.new(pattern)
291
- fetch_items_from_filesystem
348
+ fetch_items_from_filesystem_or_zip
292
349
  @items = items.shift(2) + items.select {|i| i.name =~ regexp}
293
350
  sort_items_according_to_current_direction
294
351
  switch_page 0
@@ -299,15 +356,29 @@ module Rfd
299
356
 
300
357
  # Copy selected files and directories to the destination.
301
358
  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)
359
+ unless in_zip?
360
+ src = (m = marked_items).any? ? m.map(&:path) : current_item.path
361
+ FileUtils.cp_r src, expand_path(dest)
362
+ else
363
+ raise 'cping multiple items in .zip is not supported.' if selected_items.size > 1
364
+ Zip::File.open(current_zip.path) do |zip|
365
+ entry = zip.find_entry(selected_items.first.name).dup
366
+ entry.name, entry.name_length = dest, dest.size
367
+ zip.instance_variable_get(:@entry_set) << entry
368
+ end
369
+ end
304
370
  ls
305
371
  end
306
372
 
307
373
  # Move selected files and directories to the destination.
308
374
  def mv(dest)
309
- src = (m = marked_items).any? ? m.map(&:path) : current_item.path
310
- FileUtils.mv src, File.join(current_dir, dest)
375
+ unless in_zip?
376
+ src = (m = marked_items).any? ? m.map(&:path) : current_item.path
377
+ FileUtils.mv src, expand_path(dest)
378
+ else
379
+ raise 'mving multiple items in .zip is not supported.' if selected_items.size > 1
380
+ rename "#{selected_items.first.name}/#{dest}"
381
+ end
311
382
  ls
312
383
  end
313
384
 
@@ -318,22 +389,165 @@ module Rfd
318
389
  def rename(pattern)
319
390
  from, to = pattern.split '/'
320
391
  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)
392
+ unless in_zip?
393
+ selected_items.each do |item|
394
+ name = item.name.gsub from, to
395
+ FileUtils.mv item.path, File.join(current_dir, name)
396
+ end
397
+ else
398
+ Zip::File.open(current_zip.path) do |zip|
399
+ selected_items.each do |item|
400
+ name = item.name.gsub from, to
401
+ zip.rename item.name, name
402
+ end
403
+ end
404
+ end
405
+ ls
406
+ end
407
+
408
+ # Soft delete selected files and directories.
409
+ #
410
+ # If the OS is not OSX, performs the same as `delete` command.
411
+ def trash
412
+ unless in_zip?
413
+ if osx?
414
+ FileUtils.mv selected_items.map(&:path), File.expand_path('~/.Trash/')
415
+ else
416
+ #TODO support other OS
417
+ FileUtils.rm_rf selected_items.map(&:path)
418
+ end
419
+ else
420
+ return unless ask %Q[Trashing zip entries is not supported. Actually the files will be deleted. Are you sure want to proceed? (y/n)]
421
+ delete
324
422
  end
423
+ @current_row -= selected_items.count {|i| i.index <= current_row}
424
+ ls
425
+ end
426
+
427
+ # Delete selected files and directories.
428
+ def delete
429
+ unless in_zip?
430
+ FileUtils.rm_rf selected_items.map(&:path)
431
+ else
432
+ Zip::File.open(current_zip.path) do |zip|
433
+ zip.select {|e| selected_items.map(&:name).include? e.to_s}.each do |entry|
434
+ if entry.name_is_directory?
435
+ zip.dir.delete entry.to_s
436
+ else
437
+ zip.file.delete entry.to_s
438
+ end
439
+ end
440
+ end
441
+ end
442
+ @current_row -= selected_items.count {|i| i.index <= current_row}
325
443
  ls
326
444
  end
327
445
 
328
446
  # Create a new directory.
329
447
  def mkdir(dir)
330
- FileUtils.mkdir_p File.join(current_dir, dir)
448
+ unless in_zip?
449
+ FileUtils.mkdir_p File.join(current_dir, dir)
450
+ else
451
+ Zip::File.open(current_zip.path) do |zip|
452
+ zip.dir.mkdir dir
453
+ end
454
+ end
331
455
  ls
332
456
  end
333
457
 
334
458
  # Create a new empty file.
335
459
  def touch(filename)
336
- FileUtils.touch File.join(current_dir, filename)
460
+ unless in_zip?
461
+ FileUtils.touch File.join(current_dir, filename)
462
+ else
463
+ Zip::File.open(current_zip.path) do |zip|
464
+ # zip.file.open(filename, 'w') {|_f| } #HAXX this code creates an unneeded temporary file
465
+ zip.instance_variable_get(:@entry_set) << Zip::Entry.new(current_zip.path, filename)
466
+ end
467
+ end
468
+ ls
469
+ end
470
+
471
+ # Create a symlink to the current file or directory.
472
+ def symlink(name)
473
+ FileUtils.ln_s current_item.path, name
474
+ ls
475
+ end
476
+
477
+ # Copy selected files and directories' path into clipboard on OSX.
478
+ def clipboard
479
+ IO.popen('pbcopy', 'w') {|f| f << selected_items.map(&:path).join(' ')} if osx?
480
+ end
481
+
482
+ # Archive selected files and directories into a .zip file.
483
+ def zip(zipfile_name)
484
+ return unless zipfile_name
485
+ zipfile_name << '.zip' unless zipfile_name.end_with? '.zip'
486
+
487
+ Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
488
+ selected_items.each do |item|
489
+ next if item.symlink?
490
+ if item.directory?
491
+ Dir[File.join(item.path, '**/**')].each do |file|
492
+ zipfile.add file.sub("#{current_dir}/", ''), file
493
+ end
494
+ else
495
+ zipfile.add item.name, item.path
496
+ end
497
+ end
498
+ end
499
+ ls
500
+ end
501
+
502
+ # Unarchive .zip and .tar.gz files within selected files and directories into current_directory.
503
+ def unarchive
504
+ unless in_zip?
505
+ zips, gzs = selected_items.partition(&:zip?).tap {|z, others| break [z, *others.partition(&:gz?)]}
506
+ zips.each do |item|
507
+ FileUtils.mkdir_p File.join(current_dir, item.basename)
508
+ Zip::File.open(item.path) do |zip|
509
+ zip.each do |entry|
510
+ FileUtils.mkdir_p File.join(File.join(item.basename, File.dirname(entry.to_s)))
511
+ zip.extract(entry, File.join(item.basename, entry.to_s)) { true }
512
+ end
513
+ end
514
+ end
515
+ gzs.each do |item|
516
+ Zlib::GzipReader.open(item.path) do |gz|
517
+ Gem::Package::TarReader.new(gz) do |tar|
518
+ dest_dir = File.join current_dir, (gz.orig_name || item.basename).sub(/\.tar$/, '')
519
+ tar.each do |entry|
520
+ dest = nil
521
+ if entry.full_name == '././@LongLink'
522
+ dest = File.join dest_dir, entry.read.strip
523
+ next
524
+ end
525
+ dest ||= File.join dest_dir, entry.full_name
526
+ if entry.directory?
527
+ FileUtils.mkdir_p dest, :mode => entry.header.mode
528
+ elsif entry.file?
529
+ FileUtils.mkdir_p dest_dir
530
+ File.open(dest, 'wb') {|f| f.print entry.read}
531
+ FileUtils.chmod entry.header.mode, dest
532
+ elsif entry.header.typeflag == '2' # symlink
533
+ File.symlink entry.header.linkname, dest
534
+ end
535
+ unless Dir.exist? dest_dir
536
+ FileUtils.mkdir_p dest_dir
537
+ File.open(File.join(dest_dir, gz.orig_name || item.basename), 'wb') {|f| f.print gz.read}
538
+ end
539
+ end
540
+ end
541
+ end
542
+ end
543
+ else
544
+ Zip::File.open(current_zip.path) do |zip|
545
+ zip.select {|e| selected_items.map(&:name).include? e.to_s}.each do |entry|
546
+ FileUtils.mkdir_p File.join(current_zip.dir, current_zip.basename, File.dirname(entry.to_s))
547
+ zip.extract(entry, File.join(current_zip.dir, current_zip.basename, entry.to_s)) { true }
548
+ end
549
+ end
550
+ end
337
551
  ls
338
552
  end
339
553
 
@@ -349,7 +563,7 @@ module Rfd
349
563
 
350
564
  # Number of pages in the current directory.
351
565
  def total_pages
352
- items.length / max_items + 1
566
+ items.size / max_items + 1
353
567
  end
354
568
 
355
569
  # Move to the given page number.
@@ -426,15 +640,54 @@ module Rfd
426
640
  def edit
427
641
  execute_external_command do
428
642
  editor = ENV['EDITOR'] || 'vim'
429
- system %Q[#{editor} "#{current_item.path}"]
643
+ unless in_zip?
644
+ system %Q[#{editor} "#{current_item.path}"]
645
+ else
646
+ begin
647
+ tmpdir, tmpfile_name = nil
648
+ Zip::File.open(current_zip.path) do |zip|
649
+ tmpdir = Dir.mktmpdir
650
+ FileUtils.mkdir_p File.join(tmpdir, File.dirname(current_item.name))
651
+ tmpfile_name = File.join(tmpdir, current_item.name)
652
+ File.open(tmpfile_name, 'w') {|f| f.puts zip.file.read(current_item.name)}
653
+ system %Q[#{editor} "#{tmpfile_name}"]
654
+ zip.add(current_item.name, tmpfile_name) { true }
655
+ end
656
+ ls
657
+ ensure
658
+ FileUtils.remove_entry_secure tmpdir if tmpdir
659
+ end
660
+ end
430
661
  end
431
662
  end
432
663
 
433
664
  # Open current file or directory with the viewer.
434
665
  def view
666
+ pager = ENV['PAGER'] || 'less'
435
667
  execute_external_command do
436
- pager = ENV['PAGER'] || 'less'
437
- system %Q[#{pager} "#{current_item.path}"]
668
+ unless in_zip?
669
+ system %Q[#{pager} "#{current_item.path}"]
670
+ else
671
+ begin
672
+ tmpdir, tmpfile_name = nil
673
+ Zip::File.open(current_zip.path) do |zip|
674
+ tmpdir = Dir.mktmpdir
675
+ FileUtils.mkdir_p File.join(tmpdir, File.dirname(current_item.name))
676
+ tmpfile_name = File.join(tmpdir, current_item.name)
677
+ File.open(tmpfile_name, 'w') {|f| f.puts zip.file.read(current_item.name)}
678
+ end
679
+ system %Q[#{pager} "#{tmpfile_name}"]
680
+ ensure
681
+ FileUtils.remove_entry_secure tmpdir if tmpdir
682
+ end
683
+ end
684
+ end
685
+ end
686
+
687
+ def move_cursor_by_click(y: nil, x: nil)
688
+ if (idx = main.pane_index_at(y: y, x: x))
689
+ row = current_page * max_items + main.maxy * idx + y - main.begy
690
+ move_cursor row if (row >= 0) && (row < items.size)
438
691
  end
439
692
  end
440
693
 
@@ -449,6 +702,18 @@ module Rfd
449
702
  Curses.refresh
450
703
  end
451
704
 
705
+ def expand_path(path)
706
+ File.expand_path path.is_a?(Rfd::Item) ? path.path : path.start_with?('/') || path.start_with?('~') ? path : current_dir ? File.join(current_dir, path) : path
707
+ end
708
+
709
+ def osx?
710
+ @_osx ||= RbConfig::CONFIG['host_os'] =~ /darwin/
711
+ end
712
+
713
+ def in_zip?
714
+ @current_zip
715
+ end
716
+
452
717
  def debug(str)
453
718
  header_r.debug str
454
719
  end
@@ -10,13 +10,11 @@ module Rfd
10
10
  process_command_line preset_command: 'cp'
11
11
  end
12
12
 
13
- # Soft "d"elete (actually mv to the trash folder) selected files and directories.
13
+ # Soft "d"elete (actually mv to the trash folder on OSX) selected files and directories.
14
14
  def d
15
15
  if selected_items.any?
16
- if ask %Q[Are your sure want to trash #{selected_items.one? ? selected_items.first.name : "these #{selected_items.size} files"}? (y/n)]
17
- FileUtils.mv selected_items.map(&:path), File.expand_path('~/.Trash/')
18
- @current_row -= selected_items.count {|i| i.index <= current_row}
19
- ls
16
+ if ask %Q[Are you sure want to trash #{selected_items.one? ? selected_items.first.name : "these #{selected_items.size} files"}? (y/n)]
17
+ trash
20
18
  end
21
19
  end
22
20
  end
@@ -26,7 +24,7 @@ module Rfd
26
24
  edit
27
25
  end
28
26
 
29
- # "f"ind the first flie or directory of which name starts with the given String.
27
+ # "f"ind the first file or directory of which name starts with the given String.
30
28
  def f
31
29
  process_command_line preset_command: 'find'
32
30
  end
@@ -66,12 +64,16 @@ module Rfd
66
64
 
67
65
  # "o"pen selected files and directories with the OS "open" command.
68
66
  def o
69
- system "open #{selected_items.map {|i| %Q["#{i.path}"]}.join(' ')}"
67
+ if selected_items.any?
68
+ system "open #{selected_items.map {|i| %Q["#{i.path}"]}.join(' ')}"
69
+ elsif %w(. ..).include? current_item.name
70
+ system %Q[open "#{current_item.path}"]
71
+ end
70
72
  end
71
73
 
72
74
  # "q"uit the app.
73
75
  def q
74
- raise StopIteration if ask 'Are your sure want to exit? (y/n)'
76
+ raise StopIteration if ask 'Are you sure want to exit? (y/n)'
75
77
  end
76
78
 
77
79
  # "r"ename selected files and directories.
@@ -89,23 +91,41 @@ module Rfd
89
91
  process_command_line preset_command: 'touch'
90
92
  end
91
93
 
94
+ # "u"narchive .zip and .tar.gz files within selected files and directories into current_directory.
95
+ def u
96
+ unarchive
97
+ end
98
+
92
99
  # "o"pen selected files and directories with the viewer.
93
100
  def v
94
101
  view
95
102
  end
96
103
 
104
+ # Change o"w"ner of selected files and directories.
105
+ def w
106
+ process_command_line preset_command: 'chown'
107
+ end
108
+
109
+ # Archive selected files and directories into a "z"ip file.
110
+ def z
111
+ process_command_line preset_command: 'zip'
112
+ end
113
+
114
+ # "C"opy paths of selected files and directory to the "C"lipboard.
115
+ def C
116
+ clipboard
117
+ end
118
+
97
119
  # Hard "d"elete selected files and directories.
98
120
  def D
99
121
  if selected_items.any?
100
- if ask %Q[Are your sure want to delete #{selected_items.one? ? selected_items.first.name : "these #{selected_items.size} files"}? (y/n)]
101
- FileUtils.rm_rf selected_items.map(&:path)
102
- @current_row -= selected_items.count {|i| i.index <= current_row}
103
- ls
122
+ if ask %Q[Are you sure want to delete #{selected_items.one? ? selected_items.first.name : "these #{selected_items.size} files"}? (y/n)]
123
+ delete
104
124
  end
105
125
  end
106
126
  end
107
127
 
108
- # "f"ind the last flie or directory of which name starts with the given String.
128
+ # "f"ind the last file or directory of which name starts with the given String.
109
129
  def F
110
130
  process_command_line preset_command: 'find_reverse'
111
131
  end
@@ -130,6 +150,19 @@ module Rfd
130
150
  move_cursor current_page * max_items + displayed_items.size / 2
131
151
  end
132
152
 
153
+ # "O"pen terminal here.
154
+ def O
155
+ dir = current_item.directory? ? current_item.path : current_dir
156
+ system %Q[osascript -e 'tell app "Terminal"
157
+ do script "cd #{dir}"
158
+ end tell'] if osx?
159
+ end
160
+
161
+ # "S"ymlink the current file or directory
162
+ def S
163
+ process_command_line preset_command: 'symlink'
164
+ end
165
+
133
166
  # Mark or unmark "a"ll files and directories.
134
167
  def ctrl_a
135
168
  mark = marked_items.size != (items.size - 2) # exclude . and ..
@@ -207,7 +240,13 @@ module Rfd
207
240
 
208
241
  # cd into a directory, or view a file.
209
242
  def enter
210
- if current_item.directory?
243
+ if current_item.name == '.' # do nothing
244
+ elsif current_item.name == '..'
245
+ cd '..'
246
+ ls
247
+ elsif in_zip?
248
+ v
249
+ elsif current_item.directory? || current_item.zip?
211
250
  cd current_item
212
251
  ls
213
252
  else
@@ -230,5 +269,17 @@ module Rfd
230
269
  ls
231
270
  end
232
271
  end
272
+
273
+ # Move cursor position by mouse click.
274
+ def click(y: nil, x: nil)
275
+ move_cursor_by_click y: y, x: x
276
+ end
277
+
278
+ # Move cursor position and enter
279
+ def double_click(y: nil, x: nil)
280
+ if move_cursor_by_click y: y, x: x
281
+ enter
282
+ end
283
+ end
233
284
  end
234
285
  end
@@ -1,11 +1,11 @@
1
1
  module Rfd
2
2
  class Item
3
3
  include Comparable
4
- attr_reader :name
4
+ attr_reader :name, :dir, :stat
5
5
  attr_accessor :index
6
6
 
7
- def initialize(dir: nil, name: nil, window_width: nil)
8
- @dir, @name, @window_width, @marked = dir, name, window_width, false
7
+ def initialize(dir: nil, name: nil, stat: nil, window_width: nil)
8
+ @dir, @name, @stat, @window_width, @marked = dir, name, stat, window_width, false
9
9
  end
10
10
 
11
11
  def path
@@ -35,20 +35,12 @@ module Rfd
35
35
  if symlink?
36
36
  mb_left n, @window_width - 16
37
37
  else
38
- "#{mb_left(basename, @window_width - 16 - extname.length)}…#{extname}"
38
+ "#{mb_left(basename, @window_width - 16 - extname.size)}…#{extname}"
39
39
  end
40
40
  end
41
41
  end
42
42
  end
43
43
 
44
- def lstat
45
- @lstat ||= File.lstat path
46
- end
47
-
48
- def stat
49
- @stat ||= File.stat path
50
- end
51
-
52
44
  def color
53
45
  if symlink?
54
46
  Curses::COLOR_MAGENTA
@@ -64,7 +56,7 @@ module Rfd
64
56
  end
65
57
 
66
58
  def size
67
- directory? ? 0 : lstat.size
59
+ directory? ? 0 : stat.size
68
60
  end
69
61
 
70
62
  def size_or_dir
@@ -72,20 +64,20 @@ module Rfd
72
64
  end
73
65
 
74
66
  def atime
75
- lstat.atime.strftime('%Y-%m-%d %H:%M:%S')
67
+ stat.atime.strftime('%Y-%m-%d %H:%M:%S')
76
68
  end
77
69
 
78
70
  def ctime
79
- lstat.ctime.strftime('%Y-%m-%d %H:%M:%S')
71
+ stat.ctime.strftime('%Y-%m-%d %H:%M:%S')
80
72
  end
81
73
 
82
74
  def mtime
83
- lstat.mtime.strftime('%Y-%m-%d %H:%M:%S')
75
+ stat.mtime.strftime('%Y-%m-%d %H:%M:%S')
84
76
  end
85
77
 
86
78
  def mode
87
79
  @mode ||= begin
88
- m = lstat.mode
80
+ m = stat.mode
89
81
  ret = directory? ? 'd' : symlink? ? 'l' : '-'
90
82
  [(m & 0700) / 64, (m & 070) / 8, m & 07].inject(ret) do |str, s|
91
83
  str << "#{s & 4 == 4 ? 'r' : '-'}#{s & 2 == 2 ? 'w' : '-'}#{s & 1 == 1 ? 'x' : '-'}"
@@ -104,11 +96,15 @@ module Rfd
104
96
  end
105
97
 
106
98
  def directory?
107
- stat.directory?
99
+ @directory ||= if symlink?
100
+ File.stat(path).directory?
101
+ else
102
+ stat.directory?
103
+ end
108
104
  end
109
105
 
110
106
  def symlink?
111
- lstat.symlink?
107
+ stat.symlink?
112
108
  end
113
109
 
114
110
  def hidden?
@@ -116,7 +112,35 @@ module Rfd
116
112
  end
117
113
 
118
114
  def executable?
119
- lstat.executable?
115
+ stat.executable?
116
+ end
117
+
118
+ def zip?
119
+ @zip_ ||= begin
120
+ if directory?
121
+ false
122
+ elsif symlink?
123
+ File.binread(target, 4).unpack('V').first == 0x04034b50
124
+ else
125
+ File.binread(path, 4).unpack('V').first == 0x04034b50
126
+ end
127
+ rescue
128
+ false
129
+ end
130
+ end
131
+
132
+ def gz?
133
+ @gz_ ||= begin
134
+ if directory?
135
+ false
136
+ elsif symlink?
137
+ File.binread(target, 2).unpack('n').first == 0x1f8b
138
+ else
139
+ File.binread(path, 2).unpack('n').first == 0x1f8b
140
+ end
141
+ rescue
142
+ false
143
+ end
120
144
  end
121
145
 
122
146
  def target
@@ -126,6 +126,13 @@ module Rfd
126
126
  @current_index = index if index < @panes.size
127
127
  end
128
128
 
129
+ def get_index_by_point(y: nil, x: nil)
130
+ @panes.each.with_index do |p, i|
131
+ return i if include_point? pane: p, y: y, x: x
132
+ end if y && x
133
+ nil
134
+ end
135
+
129
136
  def size
130
137
  @panes.size
131
138
  end
@@ -133,6 +140,10 @@ module Rfd
133
140
  def close_all
134
141
  @panes.each {|p| Curses.delwin p}
135
142
  end
143
+
144
+ def include_point?(pane: pane, y: nil, x: nil)
145
+ (y >= Curses.getbegy(pane)) && (Curses.getbegy(pane) + Curses.getmaxy(pane) > y) && (x >= Curses.getbegx(pane)) && (Curses.getbegx(pane) + Curses.getmaxx(pane) > x)
146
+ end
136
147
  end
137
148
 
138
149
  def initialize(dir = '.')
@@ -155,6 +166,10 @@ module Rfd
155
166
  @panes.activate num
156
167
  end
157
168
 
169
+ def pane_index_at(y: nil, x: nil)
170
+ @panes.get_index_by_point y: y, x: x
171
+ end
172
+
158
173
  def window
159
174
  @panes.active
160
175
  end
@@ -207,7 +222,7 @@ module Rfd
207
222
 
208
223
  def get_command(prompt: nil)
209
224
  Curses.echo
210
- startx = prompt ? prompt.length : 1
225
+ startx = prompt ? prompt.size : 1
211
226
  s = ' ' * 100
212
227
  Curses.mvwgetstr window, 0, startx, s
213
228
  "#{prompt[1..-1] if prompt}#{s.strip}"
@@ -4,11 +4,11 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "rfd"
7
- spec.version = '0.0.1'
7
+ spec.version = '0.1.0'
8
8
  spec.authors = ["Akira Matsuda"]
9
9
  spec.email = ["ronnie@dio.jp"]
10
- spec.description = 'Ruby on Files and Directories'
11
- spec.summary = 'Ruby on Files and Directories'
10
+ spec.description = 'Ruby on Files & Directories'
11
+ spec.summary = 'Ruby on Files & Directories'
12
12
  spec.homepage = 'https://github.com/amatsuda/rfd'
13
13
  spec.license = "MIT"
14
14
 
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_dependency 'ffi-ncurses'
21
+ spec.add_dependency 'rubyzip', '>= 1.0.0'
21
22
  spec.add_development_dependency "bundler", "~> 1.3"
22
23
  spec.add_development_dependency "rake"
23
24
  spec.add_development_dependency 'rspec'
@@ -6,12 +6,16 @@ describe Rfd::Controller do
6
6
 
7
7
  around do |example|
8
8
  @stdout = capture(:stdout) do
9
- @rfd = Rfd.start testdir
9
+ FileUtils.cp_r File.join(__dir__, 'testdir'), tmpdir
10
+ @rfd = Rfd.start tmpdir
10
11
  def (@rfd.main).maxy
11
12
  3
12
13
  end
13
14
 
14
15
  example.run
16
+
17
+ FileUtils.rm_r tmpdir
18
+ Dir.chdir __dir__
15
19
  end
16
20
  end
17
21
 
@@ -19,7 +23,7 @@ describe Rfd::Controller do
19
23
  Curses.endwin
20
24
  end
21
25
 
22
- let(:testdir) { File.join __dir__, 'testdir' }
26
+ let(:tmpdir) { File.join __dir__, 'tmpdir' }
23
27
  let!(:controller) { @rfd }
24
28
  subject { controller }
25
29
  let(:items) { controller.items }
@@ -102,13 +106,13 @@ describe Rfd::Controller do
102
106
  before do
103
107
  controller.cd 'dir1'
104
108
  end
105
- its(:current_dir) { should == File.join(testdir, 'dir1') }
109
+ its(:current_dir) { should == File.join(tmpdir, 'dir1') }
106
110
 
107
111
  describe '#popd' do
108
112
  before do
109
113
  controller.popd
110
114
  end
111
- its(:current_dir) { should == testdir }
115
+ its(:current_dir) { should == tmpdir }
112
116
  end
113
117
  end
114
118
 
@@ -122,30 +126,76 @@ describe Rfd::Controller do
122
126
 
123
127
  describe '#sort' do
124
128
  let(:item) do
125
- Dir.mkdir File.join testdir, '.a'
126
- Rfd::Item.new dir: testdir, name: '.a', window_width: 100
129
+ Dir.mkdir File.join(tmpdir, '.a')
130
+ stat = File.lstat File.join(tmpdir, '.a')
131
+ Rfd::Item.new dir: tmpdir, name: '.a', stat: stat, window_width: 100
127
132
  end
128
133
  before do
129
134
  controller.items << item
130
135
  controller.sort
131
136
  end
132
- after do
133
- Dir.rmdir File.join testdir, '.a'
134
- end
135
137
  subject { item }
136
138
  its(:index) { should == 2 } # . .. then next
137
139
  end
138
140
 
139
141
  describe '#chmod' do
140
142
  let(:item) { controller.items.detect {|i| !i.directory?} }
141
- before do
142
- item.toggle_mark
143
- controller.chmod 'a+w'
144
- item.instance_variable_set :@lstat, nil # clear cached value
145
- item.instance_variable_set :@mode, nil # clear cached value
143
+
144
+ context 'With an octet string' do
145
+ before do
146
+ item.toggle_mark
147
+ controller.chmod '666'
148
+ end
149
+ subject { controller.items.detect {|i| !i.directory?} }
150
+ its(:mode) { should == '-rw-rw-rw-' }
146
151
  end
152
+
153
+ context 'With a decimal string' do
154
+ before do
155
+ item.toggle_mark
156
+ controller.chmod '0666'
157
+ end
158
+ subject { controller.items.detect {|i| !i.directory?} }
159
+ its(:mode) { should == '-rw-rw-rw-' }
160
+ end
161
+
162
+ context 'With a non-numeric string' do
163
+ before do
164
+ item.toggle_mark
165
+ controller.chmod 'a+w'
166
+ end
167
+ subject { controller.items.detect {|i| !i.directory?} }
168
+ its(:mode) { should == '-rw-rw-rw-' }
169
+ end
170
+ end
171
+
172
+ describe '#chown' do
173
+ let(:item) { controller.items.detect {|i| !i.directory?} }
147
174
  subject { item }
148
- its(:mode) { should == '-rw-rw-rw-' }
175
+
176
+ context 'With user name only' do
177
+ before do
178
+ expect(FileUtils).to receive(:chown).with('alice', nil, Array(item.path))
179
+ item.toggle_mark
180
+ end
181
+ specify { controller.chown 'alice' }
182
+ end
183
+
184
+ context 'With group name only' do
185
+ before do
186
+ expect(FileUtils).to receive(:chown).with(nil, 'admin', Array(item.path))
187
+ item.toggle_mark
188
+ end
189
+ specify { controller.chown ':admin' }
190
+ end
191
+
192
+ context 'With both user name and group name' do
193
+ before do
194
+ expect(FileUtils).to receive(:chown).with('nobody', 'nobody', Array(item.path))
195
+ item.toggle_mark
196
+ end
197
+ specify { controller.chown 'nobody:nobody' }
198
+ end
149
199
  end
150
200
 
151
201
  describe '#find' do
@@ -178,11 +228,8 @@ describe Rfd::Controller do
178
228
  controller.find 'file1'
179
229
  controller.cp 'file4'
180
230
  end
181
- after do
182
- File.delete File.join(testdir, 'file4')
183
- end
184
231
  it 'should be the same file as the copy source file' do
185
- File.read(File.join(testdir, 'file1')).should == File.read(File.join(testdir, 'file4'))
232
+ File.read(File.join(tmpdir, 'file1')).should == File.read(File.join(tmpdir, 'file4'))
186
233
  end
187
234
  end
188
235
 
@@ -191,12 +238,8 @@ describe Rfd::Controller do
191
238
  controller.find 'file3'
192
239
  controller.mv 'dir2'
193
240
  end
194
- after do
195
- FileUtils.mv File.join(testdir, 'dir2/file3'), File.join(testdir, 'file3')
196
- end
197
- it 'should move current file to the specified directory' do
198
- File.exist?(File.join(testdir, 'dir2/file3')).should == true
199
- end
241
+ subject { File }
242
+ it { should be_exist File.join(tmpdir, 'dir2/file3') }
200
243
  end
201
244
 
202
245
  describe '#rename' do
@@ -207,13 +250,33 @@ describe Rfd::Controller do
207
250
  controller.toggle_mark
208
251
  controller.rename 'fi/faaai'
209
252
  end
210
- after do
211
- FileUtils.mv File.join(testdir, '.faaaile2'), File.join(testdir, '.file2')
212
- FileUtils.mv File.join(testdir, 'faaaile3'), File.join(testdir, 'file3')
253
+ subject { File }
254
+ it { should be_exist File.join(tmpdir, '.faaaile2') }
255
+ it { should be_exist File.join(tmpdir, 'faaaile3') }
256
+ end
257
+
258
+ describe '#trash' do
259
+ before do
260
+ controller.find 'file3'
261
+ controller.toggle_mark
262
+ controller.trash
263
+ end
264
+ it 'should be properly deleted from the current directory' do
265
+ controller.items.should be_none {|i| i.name == 'file3'}
213
266
  end
214
- it 'should rename selected files' do
215
- File.exist?(File.join(testdir, '.faaaile2')).should == true
216
- File.exist?(File.join(testdir, 'faaaile3')).should == true
267
+ end
268
+
269
+ describe '#delete' do
270
+ before do
271
+ controller.find 'file3'
272
+ controller.toggle_mark
273
+ controller.find 'dir2'
274
+ controller.toggle_mark
275
+ controller.delete
276
+ end
277
+ it 'should be properly deleted from the current directory' do
278
+ controller.items.should be_none {|i| i.name == 'file3'}
279
+ controller.items.should be_none {|i| i.name == 'dir2'}
217
280
  end
218
281
  end
219
282
 
@@ -221,24 +284,62 @@ describe Rfd::Controller do
221
284
  before do
222
285
  controller.mkdir 'aho'
223
286
  end
224
- after do
225
- Dir.delete File.join(testdir, 'aho')
226
- end
227
- it 'should create a new directory' do
228
- Dir.exist?(File.join(testdir, 'aho')).should == true
229
- end
287
+ subject { Dir }
288
+ it { should be_exist File.join(tmpdir, 'aho') }
230
289
  end
231
290
 
232
291
  describe '#touch' do
233
292
  before do
234
293
  controller.touch 'fuga'
235
294
  end
236
- after do
237
- File.delete File.join(testdir, 'fuga')
295
+ subject { File }
296
+ it { should be_exist File.join(tmpdir, 'fuga') }
297
+ end
298
+
299
+ describe '#symlink' do
300
+ before do
301
+ controller.find 'dir1'
302
+ controller.symlink 'aaa'
238
303
  end
239
- it 'should create a new file' do
240
- File.exist?(File.join(testdir, 'fuga')).should == true
304
+ subject { File }
305
+ it { should be_symlink File.join(tmpdir, 'aaa') }
306
+ end
307
+
308
+ describe '#pbcopy' do
309
+ before do
310
+ controller.find '.file1'
311
+ controller.toggle_mark
312
+ controller.find 'dir3'
313
+ controller.toggle_mark
314
+ controller.clipboard
315
+ end
316
+ it 'copies the selected paths into clipboard' do
317
+ `pbpaste`.should == "#{File.join(tmpdir, 'dir3')} #{File.join(tmpdir, '.file1')}"
318
+ end
319
+ end
320
+
321
+ describe '#zip' do
322
+ before do
323
+ controller.find 'dir1'
324
+ controller.zip 'archive1'
325
+ end
326
+ subject { File }
327
+ it { should be_exist File.join(tmpdir, 'archive1.zip') }
328
+ end
329
+
330
+ describe '#unarchive' do
331
+ before do
332
+ controller.find 'zip1'
333
+ controller.toggle_mark
334
+ controller.find 'gz1'
335
+ controller.toggle_mark
336
+ controller.unarchive
241
337
  end
338
+ subject { File }
339
+ it { should be_exist File.join(tmpdir, 'zip1/zip_content1') }
340
+ it { should be_exist File.join(tmpdir, 'zip1/zip_content_dir1/zip_content1_1') }
341
+ it { should be_exist File.join(tmpdir, 'gz1/gz_content1') }
342
+ it { should be_exist File.join(tmpdir, 'gz1/gz_content_dir1/gz_content1_1') }
242
343
  end
243
344
 
244
345
  describe '#first_page? and #last_page?' do
@@ -4,6 +4,3 @@ $LOAD_PATH.unshift(__dir__)
4
4
  require 'rfd'
5
5
 
6
6
  Dir[File.join __dir__, 'support/**/*.rb'].each {|f| require f}
7
-
8
- RSpec.configure do |config|
9
- end
Binary file
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Akira Matsuda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-08 00:00:00.000000000 Z
11
+ date: 2013-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi-ncurses
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubyzip
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,7 +80,7 @@ dependencies:
66
80
  - - '>='
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
69
- description: Ruby on Files and Directories
83
+ description: Ruby on Files & Directories
70
84
  email:
71
85
  - ronnie@dio.jp
72
86
  executables:
@@ -95,8 +109,10 @@ files:
95
109
  - spec/testdir/file1
96
110
  - spec/testdir/file2
97
111
  - spec/testdir/file3
112
+ - spec/testdir/gz1.tar.gz
98
113
  - spec/testdir/link1
99
114
  - spec/testdir/link2
115
+ - spec/testdir/zip1.zip
100
116
  homepage: https://github.com/amatsuda/rfd
101
117
  licenses:
102
118
  - MIT
@@ -120,7 +136,7 @@ rubyforge_project:
120
136
  rubygems_version: 2.0.3
121
137
  signing_key:
122
138
  specification_version: 4
123
- summary: Ruby on Files and Directories
139
+ summary: Ruby on Files & Directories
124
140
  test_files:
125
141
  - spec/controller_spec.rb
126
142
  - spec/spec_helper.rb
@@ -132,6 +148,8 @@ test_files:
132
148
  - spec/testdir/file1
133
149
  - spec/testdir/file2
134
150
  - spec/testdir/file3
151
+ - spec/testdir/gz1.tar.gz
135
152
  - spec/testdir/link1
136
153
  - spec/testdir/link2
154
+ - spec/testdir/zip1.zip
137
155
  has_rdoc: