rfd 0.0.1 → 0.1.0

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