droxi 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/Rakefile +28 -16
 - data/bin/droxi +1 -1
 - data/droxi.1.template +6 -1
 - data/droxi.gemspec +3 -3
 - data/lib/droxi/commands.rb +81 -62
 - data/lib/droxi/complete.rb +82 -0
 - data/lib/droxi/settings.rb +13 -4
 - data/lib/droxi/state.rb +44 -54
 - data/lib/droxi/text.rb +67 -0
 - data/lib/droxi.rb +39 -61
 - data/spec/commands_spec.rb +16 -9
 - data/spec/complete_spec.rb +114 -0
 - data/spec/state_spec.rb +4 -4
 - data/spec/text_spec.rb +60 -0
 - metadata +7 -3
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA1:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: dd71a917fd5c4fd91e3193c2805df9759505ac95
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: b5a4517dbc2ff604120b559919bc2750616acbfb
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: b547ebc0138aa9e79bcfd97b47a6809d2b63af71493b45e8cca19136b063f2f8fcf91c45216cdbdb617e7e05c1dbffb68d0bc33409ec6efae73ae5c91c21d658
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 43011ae5570b2eafcfbe7124a56f0422fe84136b6937e8a72d782e96887b952433f7f2ba1519912bc2c501e15e4e934f09dbbe0c0698dceffa4c0699cf875c77
         
     | 
    
        data/Rakefile
    CHANGED
    
    | 
         @@ -18,10 +18,13 @@ task :gem do 
     | 
|
| 
       18 
18 
     | 
    
         
             
              sh 'gem install ./droxi-*.gem'
         
     | 
| 
       19 
19 
     | 
    
         
             
            end
         
     | 
| 
       20 
20 
     | 
    
         | 
| 
      
 21 
     | 
    
         
            +
            desc 'create rdoc documentation'
         
     | 
| 
      
 22 
     | 
    
         
            +
            task :doc do
         
     | 
| 
      
 23 
     | 
    
         
            +
              sh 'rdoc `find lib -name *.rb`'
         
     | 
| 
      
 24 
     | 
    
         
            +
            end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
       21 
26 
     | 
    
         
             
            desc 'install man page (must have root permissions)'
         
     | 
| 
       22 
27 
     | 
    
         
             
            task :man do
         
     | 
| 
       23 
     | 
    
         
            -
              gemspec = IO.read('droxi.gemspec')
         
     | 
| 
       24 
     | 
    
         
            -
             
     | 
| 
       25 
28 
     | 
    
         
             
              def date(gemspec)
         
     | 
| 
       26 
29 
     | 
    
         
             
                require 'time'
         
     | 
| 
       27 
30 
     | 
    
         
             
                Time.parse(/\d{4}-\d{2}-\d{2}/.match(gemspec)[0]).strftime('%B %Y')
         
     | 
| 
         @@ -35,22 +38,31 @@ task :man do 
     | 
|
| 
       35 
38 
     | 
    
         
             
                end.join.strip
         
     | 
| 
       36 
39 
     | 
    
         
             
              end
         
     | 
| 
       37 
40 
     | 
    
         | 
| 
       38 
     | 
    
         
            -
               
     | 
| 
       39 
     | 
    
         
            -
                 
     | 
| 
       40 
     | 
    
         
            -
                sub('{version}', /\d+\.\d+\.\d+/.match(gemspec)[0]).
         
     | 
| 
       41 
     | 
    
         
            -
                sub('{commands}', commands)
         
     | 
| 
      
 41 
     | 
    
         
            +
              def build_page
         
     | 
| 
      
 42 
     | 
    
         
            +
                gemspec = IO.read('droxi.gemspec')
         
     | 
| 
       42 
43 
     | 
    
         | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
      
 44 
     | 
    
         
            +
                contents = IO.read('droxi.1.template').
         
     | 
| 
      
 45 
     | 
    
         
            +
                  sub('{date}', date(gemspec)).
         
     | 
| 
      
 46 
     | 
    
         
            +
                  sub('{version}', /\d+\.\d+\.\d+/.match(gemspec)[0]).
         
     | 
| 
      
 47 
     | 
    
         
            +
                  sub('{commands}', commands)
         
     | 
| 
       45 
48 
     | 
    
         | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
      
 49 
     | 
    
         
            +
                Dir.mkdir('build') unless Dir.exists?('build')
         
     | 
| 
      
 50 
     | 
    
         
            +
                IO.write('build/droxi.1', contents)
         
     | 
| 
      
 51 
     | 
    
         
            +
              end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
              def install_page
         
     | 
| 
      
 54 
     | 
    
         
            +
                prefix = ENV['PREFIX'] || ENV['prefix'] || '/usr/local'
         
     | 
| 
      
 55 
     | 
    
         
            +
                install_path = "#{prefix}/share/man/man1"
         
     | 
| 
       48 
56 
     | 
    
         | 
| 
       49 
     | 
    
         
            -
             
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
      
 57 
     | 
    
         
            +
                require 'fileutils'
         
     | 
| 
      
 58 
     | 
    
         
            +
                begin
         
     | 
| 
      
 59 
     | 
    
         
            +
                  FileUtils.mkdir_p(install_path)
         
     | 
| 
      
 60 
     | 
    
         
            +
                  FileUtils.cp('build/droxi.1', install_path)
         
     | 
| 
      
 61 
     | 
    
         
            +
                rescue
         
     | 
| 
      
 62 
     | 
    
         
            +
                  puts 'Failed to install man page. This target must be run as root.'
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
       55 
64 
     | 
    
         
             
              end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
              build_page
         
     | 
| 
      
 67 
     | 
    
         
            +
              install_page
         
     | 
| 
       56 
68 
     | 
    
         
             
            end
         
     | 
    
        data/bin/droxi
    CHANGED
    
    
    
        data/droxi.1.template
    CHANGED
    
    | 
         @@ -2,7 +2,12 @@ 
     | 
|
| 
       2 
2 
     | 
    
         
             
            .SH NAME
         
     | 
| 
       3 
3 
     | 
    
         
             
            droxi \- ftp-like command-line interface to Dropbox
         
     | 
| 
       4 
4 
     | 
    
         
             
            .SH SYNOPSIS
         
     | 
| 
       5 
     | 
    
         
            -
            droxi
         
     | 
| 
      
 5 
     | 
    
         
            +
            droxi [COMMAND] [ARGUMENT]...
         
     | 
| 
      
 6 
     | 
    
         
            +
            .SH DESCRIPTION
         
     | 
| 
      
 7 
     | 
    
         
            +
            A command-line Dropbox interface inspired by GNU coreutils, GNU ftp, and lftp.
         
     | 
| 
      
 8 
     | 
    
         
            +
            Features include smart tab completion, globbing, and interactive help. If
         
     | 
| 
      
 9 
     | 
    
         
            +
            invoked without arguments, runs in interactive mode. If invoked with arguments,
         
     | 
| 
      
 10 
     | 
    
         
            +
            parses the arguments as a command invocation, executes the command, and exits.
         
     | 
| 
       6 
11 
     | 
    
         
             
            .SH COMMANDS
         
     | 
| 
       7 
12 
     | 
    
         
             
            {commands}
         
     | 
| 
       8 
13 
     | 
    
         
             
            .SH AUTHOR
         
     | 
    
        data/droxi.gemspec
    CHANGED
    
    | 
         @@ -1,10 +1,10 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            Gem::Specification.new do |s|
         
     | 
| 
       2 
2 
     | 
    
         
             
              s.name        = 'droxi'
         
     | 
| 
       3 
     | 
    
         
            -
              s.version     = '0.0. 
     | 
| 
      
 3 
     | 
    
         
            +
              s.version     = '0.0.2'
         
     | 
| 
       4 
4 
     | 
    
         
             
              s.date        = '2014-06-02'
         
     | 
| 
       5 
5 
     | 
    
         
             
              s.summary     = 'ftp-like command-line interface to Dropbox'
         
     | 
| 
       6 
     | 
    
         
            -
              s.description = " 
     | 
| 
       7 
     | 
    
         
            -
                               coreutils, GNU ftp, and lftp.  
     | 
| 
      
 6 
     | 
    
         
            +
              s.description = "A command-line Dropbox interface inspired by GNU \
         
     | 
| 
      
 7 
     | 
    
         
            +
                               coreutils, GNU ftp, and lftp. Features include smart tab \
         
     | 
| 
       8 
8 
     | 
    
         
             
                               completion, globbing, and interactive help.".squeeze(' ')
         
     | 
| 
       9 
9 
     | 
    
         
             
              s.authors     = ['Brandon Mulcahy']
         
     | 
| 
       10 
10 
     | 
    
         
             
              s.email       = 'brandon@jangler.info'
         
     | 
    
        data/lib/droxi/commands.rb
    CHANGED
    
    | 
         @@ -1,18 +1,38 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            require_relative 'text'
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            # Module containing definitions for client commands.
         
     | 
| 
       3 
4 
     | 
    
         
             
            module Commands
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              # Exception indicating that a client command was given the wrong number of
         
     | 
| 
      
 7 
     | 
    
         
            +
              # arguments.
         
     | 
| 
       4 
8 
     | 
    
         
             
              class UsageError < ArgumentError
         
     | 
| 
       5 
9 
     | 
    
         
             
              end
         
     | 
| 
       6 
10 
     | 
    
         | 
| 
      
 11 
     | 
    
         
            +
              # A client command. Contains metadata as well as execution procedure.
         
     | 
| 
       7 
12 
     | 
    
         
             
              class Command
         
     | 
| 
       8 
     | 
    
         
            -
                attr_reader :usage, :description
         
     | 
| 
       9 
13 
     | 
    
         | 
| 
      
 14 
     | 
    
         
            +
                # A +String+ specifying the usage of the command in the style of a man page
         
     | 
| 
      
 15 
     | 
    
         
            +
                # synopsis. Optional arguments are enclosed in brackets; varargs-style
         
     | 
| 
      
 16 
     | 
    
         
            +
                # arguments are suffixed with an ellipsis.
         
     | 
| 
      
 17 
     | 
    
         
            +
                attr_reader :usage
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # A complete description of the command, suitable for display to the end
         
     | 
| 
      
 20 
     | 
    
         
            +
                # user.
         
     | 
| 
      
 21 
     | 
    
         
            +
                attr_reader :description
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                # Create a new +Command+ with the given metadata and a +Proc+ specifying
         
     | 
| 
      
 24 
     | 
    
         
            +
                # its behavior. The +Proc+ will receive four arguments: the
         
     | 
| 
      
 25 
     | 
    
         
            +
                # +DropboxClient+, the +State+, an +Array+ of command-line arguments, and
         
     | 
| 
      
 26 
     | 
    
         
            +
                # a +Proc+ to be called for output.
         
     | 
| 
       10 
27 
     | 
    
         
             
                def initialize(usage, description, procedure)
         
     | 
| 
       11 
28 
     | 
    
         
             
                  @usage = usage
         
     | 
| 
       12 
29 
     | 
    
         
             
                  @description = description.squeeze(' ')
         
     | 
| 
       13 
30 
     | 
    
         
             
                  @procedure = procedure
         
     | 
| 
       14 
31 
     | 
    
         
             
                end
         
     | 
| 
       15 
32 
     | 
    
         | 
| 
      
 33 
     | 
    
         
            +
                # Attempt to execute the +Command+, yielding lines of output if a block is
         
     | 
| 
      
 34 
     | 
    
         
            +
                # given. Raises a +UsageError+ if an invalid number of command-line
         
     | 
| 
      
 35 
     | 
    
         
            +
                # arguments is given.
         
     | 
| 
       16 
36 
     | 
    
         
             
                def exec(client, state, *args)
         
     | 
| 
       17 
37 
     | 
    
         
             
                  if num_args_ok?(args.length)
         
     | 
| 
       18 
38 
     | 
    
         
             
                    block = proc { |line| yield line if block_given? }
         
     | 
| 
         @@ -22,6 +42,21 @@ module Commands 
     | 
|
| 
       22 
42 
     | 
    
         
             
                  end
         
     | 
| 
       23 
43 
     | 
    
         
             
                end
         
     | 
| 
       24 
44 
     | 
    
         | 
| 
      
 45 
     | 
    
         
            +
                # Return a +String+ describing the type of argument at the given index.
         
     | 
| 
      
 46 
     | 
    
         
            +
                # If the index is out of range, return the type of the final argument. If
         
     | 
| 
      
 47 
     | 
    
         
            +
                # the +Command+ takes no arguments, return +nil+.
         
     | 
| 
      
 48 
     | 
    
         
            +
                def type_of_arg(index)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  args = @usage.split.drop(1)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  if args.empty?
         
     | 
| 
      
 51 
     | 
    
         
            +
                    nil
         
     | 
| 
      
 52 
     | 
    
         
            +
                  else
         
     | 
| 
      
 53 
     | 
    
         
            +
                    index = [index, args.length - 1].min
         
     | 
| 
      
 54 
     | 
    
         
            +
                    args[index].tr('[].', '')
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                private
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
       25 
60 
     | 
    
         
             
                def num_args_ok?(num_args)
         
     | 
| 
       26 
61 
     | 
    
         
             
                  args = @usage.split.drop(1)
         
     | 
| 
       27 
62 
     | 
    
         
             
                  min_args = args.reject { |arg| arg.start_with?('[') }.length
         
     | 
| 
         @@ -34,14 +69,9 @@ module Commands 
     | 
|
| 
       34 
69 
     | 
    
         
             
                  end
         
     | 
| 
       35 
70 
     | 
    
         
             
                  (min_args..max_args).include?(num_args)
         
     | 
| 
       36 
71 
     | 
    
         
             
                end
         
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
                def type_of_arg(index)
         
     | 
| 
       39 
     | 
    
         
            -
                  args = @usage.split.drop(1)
         
     | 
| 
       40 
     | 
    
         
            -
                  index = [index, args.length - 1].min
         
     | 
| 
       41 
     | 
    
         
            -
                  args[index].tr('[].', '')
         
     | 
| 
       42 
     | 
    
         
            -
                end
         
     | 
| 
       43 
72 
     | 
    
         
             
              end
         
     | 
| 
       44 
73 
     | 
    
         | 
| 
      
 74 
     | 
    
         
            +
              # Change the remote working directory.
         
     | 
| 
       45 
75 
     | 
    
         
             
              CD = Command.new(
         
     | 
| 
       46 
76 
     | 
    
         
             
                'cd [REMOTE_DIR]',
         
     | 
| 
       47 
77 
     | 
    
         
             
                "Change the remote working directory. With no arguments, changes to the \
         
     | 
| 
         @@ -55,7 +85,7 @@ module Commands 
     | 
|
| 
       55 
85 
     | 
    
         
             
                    state.pwd = state.oldpwd
         
     | 
| 
       56 
86 
     | 
    
         
             
                  else
         
     | 
| 
       57 
87 
     | 
    
         
             
                    path = state.resolve_path(args[0])
         
     | 
| 
       58 
     | 
    
         
            -
                    if state. 
     | 
| 
      
 88 
     | 
    
         
            +
                    if state.directory?(path)
         
     | 
| 
       59 
89 
     | 
    
         
             
                      state.pwd = path
         
     | 
| 
       60 
90 
     | 
    
         
             
                    else
         
     | 
| 
       61 
91 
     | 
    
         
             
                      output.call('Not a directory')
         
     | 
| 
         @@ -64,6 +94,7 @@ module Commands 
     | 
|
| 
       64 
94 
     | 
    
         
             
                end
         
     | 
| 
       65 
95 
     | 
    
         
             
              )
         
     | 
| 
       66 
96 
     | 
    
         | 
| 
      
 97 
     | 
    
         
            +
              # Terminate the session.
         
     | 
| 
       67 
98 
     | 
    
         
             
              EXIT = Command.new(
         
     | 
| 
       68 
99 
     | 
    
         
             
                'exit',
         
     | 
| 
       69 
100 
     | 
    
         
             
                "Exit the program.",
         
     | 
| 
         @@ -72,17 +103,19 @@ module Commands 
     | 
|
| 
       72 
103 
     | 
    
         
             
                end
         
     | 
| 
       73 
104 
     | 
    
         
             
              )
         
     | 
| 
       74 
105 
     | 
    
         | 
| 
      
 106 
     | 
    
         
            +
              # Download remote files.
         
     | 
| 
       75 
107 
     | 
    
         
             
              GET = Command.new(
         
     | 
| 
       76 
108 
     | 
    
         
             
                'get REMOTE_FILE...',
         
     | 
| 
       77 
109 
     | 
    
         
             
                "Download each specified remote file to a file of the same name in the \
         
     | 
| 
       78 
110 
     | 
    
         
             
                 local working directory.",
         
     | 
| 
       79 
111 
     | 
    
         
             
                lambda do |client, state, args, output|
         
     | 
| 
       80 
     | 
    
         
            -
                  state.expand_patterns( 
     | 
| 
      
 112 
     | 
    
         
            +
                  state.expand_patterns(args).each do |path|
         
     | 
| 
       81 
113 
     | 
    
         
             
                    begin
         
     | 
| 
       82 
114 
     | 
    
         
             
                      contents = client.get_file(path)
         
     | 
| 
       83 
115 
     | 
    
         
             
                      File.open(File.basename(path), 'wb') do |file|
         
     | 
| 
       84 
116 
     | 
    
         
             
                        file.write(contents)
         
     | 
| 
       85 
117 
     | 
    
         
             
                      end
         
     | 
| 
      
 118 
     | 
    
         
            +
                      output.call("#{File.basename(path)} <- #{path}")
         
     | 
| 
       86 
119 
     | 
    
         
             
                    rescue DropboxError => error
         
     | 
| 
       87 
120 
     | 
    
         
             
                      output.call(error.to_s)
         
     | 
| 
       88 
121 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -90,19 +123,20 @@ module Commands 
     | 
|
| 
       90 
123 
     | 
    
         
             
                end
         
     | 
| 
       91 
124 
     | 
    
         
             
              )
         
     | 
| 
       92 
125 
     | 
    
         | 
| 
      
 126 
     | 
    
         
            +
              # List commands, or print information about a specific command.
         
     | 
| 
       93 
127 
     | 
    
         
             
              HELP = Command.new(
         
     | 
| 
       94 
128 
     | 
    
         
             
                'help [COMMAND]',
         
     | 
| 
       95 
129 
     | 
    
         
             
                "Print usage and help information about a command. If no command is \
         
     | 
| 
       96 
130 
     | 
    
         
             
                 given, print a list of commands instead.",
         
     | 
| 
       97 
131 
     | 
    
         
             
                lambda do |client, state, args, output|
         
     | 
| 
       98 
132 
     | 
    
         
             
                  if args.empty?
         
     | 
| 
       99 
     | 
    
         
            -
                     
     | 
| 
      
 133 
     | 
    
         
            +
                    Text.table(NAMES).each { |line| output.call(line) }
         
     | 
| 
       100 
134 
     | 
    
         
             
                  else
         
     | 
| 
       101 
135 
     | 
    
         
             
                    cmd_name = args[0]
         
     | 
| 
       102 
136 
     | 
    
         
             
                    if NAMES.include?(cmd_name)
         
     | 
| 
       103 
137 
     | 
    
         
             
                      cmd = const_get(cmd_name.upcase.to_s)
         
     | 
| 
       104 
138 
     | 
    
         
             
                      output.call(cmd.usage)
         
     | 
| 
       105 
     | 
    
         
            -
                       
     | 
| 
      
 139 
     | 
    
         
            +
                      Text.wrap(cmd.description).each { |line| output.call(line) }
         
     | 
| 
       106 
140 
     | 
    
         
             
                    else
         
     | 
| 
       107 
141 
     | 
    
         
             
                      output.call("Unrecognized command: #{cmd_name}")
         
     | 
| 
       108 
142 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -110,6 +144,7 @@ module Commands 
     | 
|
| 
       110 
144 
     | 
    
         
             
                end
         
     | 
| 
       111 
145 
     | 
    
         
             
              )
         
     | 
| 
       112 
146 
     | 
    
         | 
| 
      
 147 
     | 
    
         
            +
              # Change the local working directory.
         
     | 
| 
       113 
148 
     | 
    
         
             
              LCD = Command.new(
         
     | 
| 
       114 
149 
     | 
    
         
             
                'lcd [LOCAL_DIR]',
         
     | 
| 
       115 
150 
     | 
    
         
             
                "Change the local working directory. With no arguments, changes to the \
         
     | 
| 
         @@ -134,6 +169,7 @@ module Commands 
     | 
|
| 
       134 
169 
     | 
    
         
             
                end
         
     | 
| 
       135 
170 
     | 
    
         
             
              )
         
     | 
| 
       136 
171 
     | 
    
         | 
| 
      
 172 
     | 
    
         
            +
              # List remote files.
         
     | 
| 
       137 
173 
     | 
    
         
             
              LS = Command.new(
         
     | 
| 
       138 
174 
     | 
    
         
             
                'ls [REMOTE_FILE]...',
         
     | 
| 
       139 
175 
     | 
    
         
             
                "List information about remote files. With no arguments, list the \
         
     | 
| 
         @@ -147,7 +183,7 @@ module Commands 
     | 
|
| 
       147 
183 
     | 
    
         
             
                    args.map do |path|
         
     | 
| 
       148 
184 
     | 
    
         
             
                      path = state.resolve_path(path)
         
     | 
| 
       149 
185 
     | 
    
         
             
                      begin
         
     | 
| 
       150 
     | 
    
         
            -
                        if state. 
     | 
| 
      
 186 
     | 
    
         
            +
                        if state.directory?(path)
         
     | 
| 
       151 
187 
     | 
    
         
             
                          "#{path}/*".sub('//', '/')
         
     | 
| 
       152 
188 
     | 
    
         
             
                        else
         
     | 
| 
       153 
189 
     | 
    
         
             
                          path
         
     | 
| 
         @@ -162,17 +198,35 @@ module Commands 
     | 
|
| 
       162 
198 
     | 
    
         
             
                  patterns.each do |pattern|
         
     | 
| 
       163 
199 
     | 
    
         
             
                    begin
         
     | 
| 
       164 
200 
     | 
    
         
             
                      dir = File.dirname(pattern)
         
     | 
| 
       165 
     | 
    
         
            -
                      state.contents( 
     | 
| 
      
 201 
     | 
    
         
            +
                      state.contents(dir).each do |path|
         
     | 
| 
       166 
202 
     | 
    
         
             
                        items << File.basename(path) if File.fnmatch(pattern, path)
         
     | 
| 
       167 
203 
     | 
    
         
             
                      end
         
     | 
| 
       168 
204 
     | 
    
         
             
                    rescue DropboxError => error
         
     | 
| 
       169 
205 
     | 
    
         
             
                      output.call(error.to_s)
         
     | 
| 
       170 
206 
     | 
    
         
             
                    end
         
     | 
| 
       171 
207 
     | 
    
         
             
                  end
         
     | 
| 
       172 
     | 
    
         
            -
                   
     | 
| 
      
 208 
     | 
    
         
            +
                  Text.table(items).each { |item| output.call(item) }
         
     | 
| 
       173 
209 
     | 
    
         
             
                end
         
     | 
| 
       174 
210 
     | 
    
         
             
              )
         
     | 
| 
       175 
211 
     | 
    
         | 
| 
      
 212 
     | 
    
         
            +
              # Get temporary links to remote files.
         
     | 
| 
      
 213 
     | 
    
         
            +
              MEDIA = Command.new(
         
     | 
| 
      
 214 
     | 
    
         
            +
                'media REMOTE_FILE...',
         
     | 
| 
      
 215 
     | 
    
         
            +
                "Create Dropbox links to publicly share remote files. The links are \
         
     | 
| 
      
 216 
     | 
    
         
            +
                 time-limited and link directly to the files themselves.",
         
     | 
| 
      
 217 
     | 
    
         
            +
                lambda do |client, state, args, output|
         
     | 
| 
      
 218 
     | 
    
         
            +
                  state.expand_patterns(args).each do |path|
         
     | 
| 
      
 219 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 220 
     | 
    
         
            +
                      url = client.media(path)['url']
         
     | 
| 
      
 221 
     | 
    
         
            +
                      output.call("#{File.basename(path)} -> #{url}")
         
     | 
| 
      
 222 
     | 
    
         
            +
                    rescue DropboxError => error
         
     | 
| 
      
 223 
     | 
    
         
            +
                      output.call(error.to_s)
         
     | 
| 
      
 224 
     | 
    
         
            +
                    end
         
     | 
| 
      
 225 
     | 
    
         
            +
                  end
         
     | 
| 
      
 226 
     | 
    
         
            +
                end
         
     | 
| 
      
 227 
     | 
    
         
            +
              )
         
     | 
| 
      
 228 
     | 
    
         
            +
             
     | 
| 
      
 229 
     | 
    
         
            +
              # Create a remote directory.
         
     | 
| 
       176 
230 
     | 
    
         
             
              MKDIR = Command.new(
         
     | 
| 
       177 
231 
     | 
    
         
             
                'mkdir REMOTE_DIR...',
         
     | 
| 
       178 
232 
     | 
    
         
             
                "Create remote directories.",
         
     | 
| 
         @@ -188,6 +242,7 @@ module Commands 
     | 
|
| 
       188 
242 
     | 
    
         
             
                end
         
     | 
| 
       189 
243 
     | 
    
         
             
              )
         
     | 
| 
       190 
244 
     | 
    
         | 
| 
      
 245 
     | 
    
         
            +
              # Upload a local file.
         
     | 
| 
       191 
246 
     | 
    
         
             
              PUT = Command.new(
         
     | 
| 
       192 
247 
     | 
    
         
             
                'put LOCAL_FILE [REMOTE_FILE]',
         
     | 
| 
       193 
248 
     | 
    
         
             
                "Upload a local file to a remote path. If a remote file of the same name \
         
     | 
| 
         @@ -205,7 +260,9 @@ module Commands 
     | 
|
| 
       205 
260 
     | 
    
         | 
| 
       206 
261 
     | 
    
         
             
                  begin
         
     | 
| 
       207 
262 
     | 
    
         
             
                    File.open(File.expand_path(from_path), 'rb') do |file|
         
     | 
| 
       208 
     | 
    
         
            -
                       
     | 
| 
      
 263 
     | 
    
         
            +
                      data = client.put_file(to_path, file)
         
     | 
| 
      
 264 
     | 
    
         
            +
                      state.cache[data['path']] = data
         
     | 
| 
      
 265 
     | 
    
         
            +
                      output.call("#{from_path} -> #{data['path']}")
         
     | 
| 
       209 
266 
     | 
    
         
             
                    end
         
     | 
| 
       210 
267 
     | 
    
         
             
                  rescue Exception => error
         
     | 
| 
       211 
268 
     | 
    
         
             
                    output.call(error.to_s)
         
     | 
| 
         @@ -213,11 +270,12 @@ module Commands 
     | 
|
| 
       213 
270 
     | 
    
         
             
                end
         
     | 
| 
       214 
271 
     | 
    
         
             
              )
         
     | 
| 
       215 
272 
     | 
    
         | 
| 
      
 273 
     | 
    
         
            +
              # Remove remote files.
         
     | 
| 
       216 
274 
     | 
    
         
             
              RM = Command.new(
         
     | 
| 
       217 
275 
     | 
    
         
             
                'rm REMOTE_FILE...',
         
     | 
| 
       218 
276 
     | 
    
         
             
                "Remove each specified remote file or directory.",
         
     | 
| 
       219 
277 
     | 
    
         
             
                lambda do |client, state, args, output|
         
     | 
| 
       220 
     | 
    
         
            -
                  state.expand_patterns( 
     | 
| 
      
 278 
     | 
    
         
            +
                  state.expand_patterns(args).each do |path|
         
     | 
| 
       221 
279 
     | 
    
         
             
                    begin
         
     | 
| 
       222 
280 
     | 
    
         
             
                      client.file_delete(path)
         
     | 
| 
       223 
281 
     | 
    
         
             
                      state.cache.delete(path)
         
     | 
| 
         @@ -228,6 +286,7 @@ module Commands 
     | 
|
| 
       228 
286 
     | 
    
         
             
                end
         
     | 
| 
       229 
287 
     | 
    
         
             
              )
         
     | 
| 
       230 
288 
     | 
    
         | 
| 
      
 289 
     | 
    
         
            +
              # Get permanent links to remote files.
         
     | 
| 
       231 
290 
     | 
    
         
             
              SHARE = Command.new(
         
     | 
| 
       232 
291 
     | 
    
         
             
                'share REMOTE_FILE...',
         
     | 
| 
       233 
292 
     | 
    
         
             
                "Create Dropbox links to publicly share remote files. The links are \
         
     | 
| 
         @@ -235,9 +294,10 @@ module Commands 
     | 
|
| 
       235 
294 
     | 
    
         
             
                 this method are set to expire far enough in the future so that \
         
     | 
| 
       236 
295 
     | 
    
         
             
                 expiration is effectively not an issue.",
         
     | 
| 
       237 
296 
     | 
    
         
             
                lambda do |client, state, args, output|
         
     | 
| 
       238 
     | 
    
         
            -
                  state.expand_patterns( 
     | 
| 
      
 297 
     | 
    
         
            +
                  state.expand_patterns(args).each do |path|
         
     | 
| 
       239 
298 
     | 
    
         
             
                    begin
         
     | 
| 
       240 
     | 
    
         
            -
                       
     | 
| 
      
 299 
     | 
    
         
            +
                      url = client.shares(path)['url']
         
     | 
| 
      
 300 
     | 
    
         
            +
                      output.call("#{File.basename(path)} -> #{url}")
         
     | 
| 
       241 
301 
     | 
    
         
             
                    rescue DropboxError => error
         
     | 
| 
       242 
302 
     | 
    
         
             
                      output.call(error.to_s)
         
     | 
| 
       243 
303 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -245,10 +305,12 @@ module Commands 
     | 
|
| 
       245 
305 
     | 
    
         
             
                end
         
     | 
| 
       246 
306 
     | 
    
         
             
              )
         
     | 
| 
       247 
307 
     | 
    
         | 
| 
      
 308 
     | 
    
         
            +
              # +Array+ of all command names.
         
     | 
| 
       248 
309 
     | 
    
         
             
              NAMES = constants.select do |sym|
         
     | 
| 
       249 
310 
     | 
    
         
             
                 const_get(sym).is_a?(Command)
         
     | 
| 
       250 
311 
     | 
    
         
             
              end.map { |sym| sym.to_s.downcase }
         
     | 
| 
       251 
312 
     | 
    
         | 
| 
      
 313 
     | 
    
         
            +
              # Parse and execute a line of user input in the given context.
         
     | 
| 
       252 
314 
     | 
    
         
             
              def self.exec(input, client, state)
         
     | 
| 
       253 
315 
     | 
    
         
             
                if input.start_with?('!')
         
     | 
| 
       254 
316 
     | 
    
         
             
                  shell(input[1, input.length - 1]) { |line| puts line }
         
     | 
| 
         @@ -283,14 +345,6 @@ module Commands 
     | 
|
| 
       283 
345 
     | 
    
         | 
| 
       284 
346 
     | 
    
         
             
              private
         
     | 
| 
       285 
347 
     | 
    
         | 
| 
       286 
     | 
    
         
            -
              def self.get_screen_size
         
     | 
| 
       287 
     | 
    
         
            -
                begin
         
     | 
| 
       288 
     | 
    
         
            -
                  Readline.get_screen_size[1]
         
     | 
| 
       289 
     | 
    
         
            -
                rescue NotImplementedError
         
     | 
| 
       290 
     | 
    
         
            -
                  72
         
     | 
| 
       291 
     | 
    
         
            -
                end
         
     | 
| 
       292 
     | 
    
         
            -
              end
         
     | 
| 
       293 
     | 
    
         
            -
             
     | 
| 
       294 
348 
     | 
    
         
             
              def self.shell(cmd)
         
     | 
| 
       295 
349 
     | 
    
         
             
                begin
         
     | 
| 
       296 
350 
     | 
    
         
             
                  IO.popen(cmd) do |pipe|
         
     | 
| 
         @@ -302,39 +356,4 @@ module Commands 
     | 
|
| 
       302 
356 
     | 
    
         
             
                end
         
     | 
| 
       303 
357 
     | 
    
         
             
              end
         
     | 
| 
       304 
358 
     | 
    
         | 
| 
       305 
     | 
    
         
            -
              def self.table_output(items)
         
     | 
| 
       306 
     | 
    
         
            -
                return [] if items.empty?
         
     | 
| 
       307 
     | 
    
         
            -
                columns = get_screen_size
         
     | 
| 
       308 
     | 
    
         
            -
                item_width = items.map { |item| item.length }.max + 2
         
     | 
| 
       309 
     | 
    
         
            -
                column = 0
         
     | 
| 
       310 
     | 
    
         
            -
                lines = ['']
         
     | 
| 
       311 
     | 
    
         
            -
                items.each do |item|
         
     | 
| 
       312 
     | 
    
         
            -
                  if column != 0 && column + item_width >= columns
         
     | 
| 
       313 
     | 
    
         
            -
                    lines << ''
         
     | 
| 
       314 
     | 
    
         
            -
                    column = 0
         
     | 
| 
       315 
     | 
    
         
            -
                  end
         
     | 
| 
       316 
     | 
    
         
            -
                  lines.last << item.ljust(item_width)
         
     | 
| 
       317 
     | 
    
         
            -
                  column += item_width
         
     | 
| 
       318 
     | 
    
         
            -
                end
         
     | 
| 
       319 
     | 
    
         
            -
                lines
         
     | 
| 
       320 
     | 
    
         
            -
              end
         
     | 
| 
       321 
     | 
    
         
            -
             
     | 
| 
       322 
     | 
    
         
            -
              def self.wrap_output(text)
         
     | 
| 
       323 
     | 
    
         
            -
                columns = get_screen_size
         
     | 
| 
       324 
     | 
    
         
            -
                column = 0
         
     | 
| 
       325 
     | 
    
         
            -
                lines = ['']
         
     | 
| 
       326 
     | 
    
         
            -
                text.split.each do |word|
         
     | 
| 
       327 
     | 
    
         
            -
                  if column != 0 && column + word.length >= columns
         
     | 
| 
       328 
     | 
    
         
            -
                    lines << ''
         
     | 
| 
       329 
     | 
    
         
            -
                    column = 0
         
     | 
| 
       330 
     | 
    
         
            -
                  end
         
     | 
| 
       331 
     | 
    
         
            -
                  if column != 0
         
     | 
| 
       332 
     | 
    
         
            -
                    lines.last << ' '
         
     | 
| 
       333 
     | 
    
         
            -
                    column += 1
         
     | 
| 
       334 
     | 
    
         
            -
                  end
         
     | 
| 
       335 
     | 
    
         
            -
                  lines.last << word
         
     | 
| 
       336 
     | 
    
         
            -
                  column += word.length
         
     | 
| 
       337 
     | 
    
         
            -
                end
         
     | 
| 
       338 
     | 
    
         
            -
                lines
         
     | 
| 
       339 
     | 
    
         
            -
              end
         
     | 
| 
       340 
359 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,82 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Module containing tab-completion logic and methods.
         
     | 
| 
      
 2 
     | 
    
         
            +
            module Complete
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
              # Return the directory in which to search for potential local tab-completions
         
     | 
| 
      
 5 
     | 
    
         
            +
              # for a +String+. Defaults to working directory in case of bogus input.
         
     | 
| 
      
 6 
     | 
    
         
            +
              def self.local_search_path(string)
         
     | 
| 
      
 7 
     | 
    
         
            +
                begin
         
     | 
| 
      
 8 
     | 
    
         
            +
                  File.expand_path(strip_filename(string))
         
     | 
| 
      
 9 
     | 
    
         
            +
                rescue
         
     | 
| 
      
 10 
     | 
    
         
            +
                  Dir.pwd
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
              end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
              # Returns an +Array+ of potential local tab-completions for a +String+.
         
     | 
| 
      
 15 
     | 
    
         
            +
              def self.local(string)
         
     | 
| 
      
 16 
     | 
    
         
            +
                dir = local_search_path(string)
         
     | 
| 
      
 17 
     | 
    
         
            +
                name = string.end_with?('/') ? '' : File.basename(string)
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                Dir.entries(dir).select do |entry|
         
     | 
| 
      
 20 
     | 
    
         
            +
                  entry.start_with?(name) && !/^\.{1,2}$/.match(entry)
         
     | 
| 
      
 21 
     | 
    
         
            +
                end.map do |entry|
         
     | 
| 
      
 22 
     | 
    
         
            +
                  entry << (File.directory?(dir + '/' + entry) ? '/' : ' ')
         
     | 
| 
      
 23 
     | 
    
         
            +
                  string + entry[name.length, entry.length]
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              # Returns an +Array+ of potential local tab-completions for a +String+,
         
     | 
| 
      
 28 
     | 
    
         
            +
              # including only directories.
         
     | 
| 
      
 29 
     | 
    
         
            +
              def self.local_dir(string)
         
     | 
| 
      
 30 
     | 
    
         
            +
                local(string).select { |result| result.end_with?('/') }
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
              # Return the directory in which to search for potential remote
         
     | 
| 
      
 34 
     | 
    
         
            +
              # tab-completions for a +String+.
         
     | 
| 
      
 35 
     | 
    
         
            +
              def self.remote_search_path(string, state)
         
     | 
| 
      
 36 
     | 
    
         
            +
                path = case
         
     | 
| 
      
 37 
     | 
    
         
            +
                when string.empty? then state.pwd + '/'
         
     | 
| 
      
 38 
     | 
    
         
            +
                when string.start_with?('/') then string
         
     | 
| 
      
 39 
     | 
    
         
            +
                else state.pwd + '/' + string
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                strip_filename(collapse(path))
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
              # Returns an +Array+ of potential remote tab-completions for a +String+.
         
     | 
| 
      
 46 
     | 
    
         
            +
              def self.remote(string, state)
         
     | 
| 
      
 47 
     | 
    
         
            +
                dir = remote_search_path(string, state)
         
     | 
| 
      
 48 
     | 
    
         
            +
                name = string.end_with?('/') ? '' : File.basename(string)
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                state.contents(dir).map do |entry|
         
     | 
| 
      
 51 
     | 
    
         
            +
                  File.basename(entry) 
         
     | 
| 
      
 52 
     | 
    
         
            +
                end.select do |entry|
         
     | 
| 
      
 53 
     | 
    
         
            +
                  entry.start_with?(name) && !/^\.{1,2}$/.match(entry)
         
     | 
| 
      
 54 
     | 
    
         
            +
                end.map do |entry|
         
     | 
| 
      
 55 
     | 
    
         
            +
                  entry << (state.directory?(dir + '/' + entry) ? '/' : ' ')
         
     | 
| 
      
 56 
     | 
    
         
            +
                  string + entry[name.length, entry.length]
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
              # Returns an +Array+ of potential remote tab-completions for a +String+,
         
     | 
| 
      
 61 
     | 
    
         
            +
              # including only directories.
         
     | 
| 
      
 62 
     | 
    
         
            +
              def self.remote_dir(string, state)
         
     | 
| 
      
 63 
     | 
    
         
            +
                remote(string, state).select { |result| result.end_with?('/') }
         
     | 
| 
      
 64 
     | 
    
         
            +
              end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
              private
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
              def self.strip_filename(path)
         
     | 
| 
      
 69 
     | 
    
         
            +
                if path != '/'
         
     | 
| 
      
 70 
     | 
    
         
            +
                  path.end_with?('/') ? path.sub(/\/$/, '') : File.dirname(path)
         
     | 
| 
      
 71 
     | 
    
         
            +
                else
         
     | 
| 
      
 72 
     | 
    
         
            +
                  path
         
     | 
| 
      
 73 
     | 
    
         
            +
                end
         
     | 
| 
      
 74 
     | 
    
         
            +
              end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
              def self.collapse(path)
         
     | 
| 
      
 77 
     | 
    
         
            +
                nil while path.sub!(/[^\/]+\/\.\.\//, '/')
         
     | 
| 
      
 78 
     | 
    
         
            +
                nil while path.sub!('./', '')
         
     | 
| 
      
 79 
     | 
    
         
            +
                path
         
     | 
| 
      
 80 
     | 
    
         
            +
              end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/droxi/settings.rb
    CHANGED
    
    | 
         @@ -1,12 +1,15 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            # Manages persistent (session-independent) application state.
         
     | 
| 
      
 2 
     | 
    
         
            +
            class Settings
         
     | 
| 
       2 
3 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
             
     | 
| 
      
 4 
     | 
    
         
            +
              # The path of the application's rc file.
         
     | 
| 
      
 5 
     | 
    
         
            +
              CONFIG_FILE_PATH = File.expand_path('~/.config/droxi/droxirc')
         
     | 
| 
       4 
6 
     | 
    
         | 
| 
       5 
     | 
    
         
            -
             
     | 
| 
      
 7 
     | 
    
         
            +
              # Return the value of a setting, or +nil+ if the setting does not exist.
         
     | 
| 
       6 
8 
     | 
    
         
             
              def Settings.[](key)
         
     | 
| 
       7 
9 
     | 
    
         
             
                @@settings[key]
         
     | 
| 
       8 
10 
     | 
    
         
             
              end
         
     | 
| 
       9 
11 
     | 
    
         | 
| 
      
 12 
     | 
    
         
            +
              # Set the value of a setting.
         
     | 
| 
       10 
13 
     | 
    
         
             
              def Settings.[]=(key, value)
         
     | 
| 
       11 
14 
     | 
    
         
             
                if value != @@settings[key]
         
     | 
| 
       12 
15 
     | 
    
         
             
                  @@dirty = true
         
     | 
| 
         @@ -14,10 +17,12 @@ class Settings 
     | 
|
| 
       14 
17 
     | 
    
         
             
                end
         
     | 
| 
       15 
18 
     | 
    
         
             
              end
         
     | 
| 
       16 
19 
     | 
    
         | 
| 
      
 20 
     | 
    
         
            +
              # Return +true+ if the setting exists, +false+ otherwise.
         
     | 
| 
       17 
21 
     | 
    
         
             
              def Settings.include?(key)
         
     | 
| 
       18 
22 
     | 
    
         
             
                @@settings.include?(key)
         
     | 
| 
       19 
23 
     | 
    
         
             
              end
         
     | 
| 
       20 
24 
     | 
    
         | 
| 
      
 25 
     | 
    
         
            +
              # Delete the setting and return its value.
         
     | 
| 
       21 
26 
     | 
    
         
             
              def Settings.delete(key)
         
     | 
| 
       22 
27 
     | 
    
         
             
                if @@settings.include?(key)
         
     | 
| 
       23 
28 
     | 
    
         
             
                  @@dirty = true
         
     | 
| 
         @@ -25,14 +30,17 @@ class Settings 
     | 
|
| 
       25 
30 
     | 
    
         
             
                end
         
     | 
| 
       26 
31 
     | 
    
         
             
              end
         
     | 
| 
       27 
32 
     | 
    
         | 
| 
       28 
     | 
    
         
            -
               
     | 
| 
      
 33 
     | 
    
         
            +
              # Write settings to disk.
         
     | 
| 
      
 34 
     | 
    
         
            +
              def Settings.save
         
     | 
| 
       29 
35 
     | 
    
         
             
                if @@dirty
         
     | 
| 
       30 
36 
     | 
    
         
             
                  @@dirty = false
         
     | 
| 
      
 37 
     | 
    
         
            +
                  require 'fileutils'
         
     | 
| 
       31 
38 
     | 
    
         
             
                  FileUtils.mkdir_p(File.dirname(CONFIG_FILE_PATH))
         
     | 
| 
       32 
39 
     | 
    
         
             
                  File.open(CONFIG_FILE_PATH, 'w') do |file|
         
     | 
| 
       33 
40 
     | 
    
         
             
                    @@settings.each_pair { |k, v| file.write("#{k}=#{v}\n") }
         
     | 
| 
       34 
41 
     | 
    
         
             
                  end
         
     | 
| 
       35 
42 
     | 
    
         
             
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
                nil
         
     | 
| 
       36 
44 
     | 
    
         
             
              end
         
     | 
| 
       37 
45 
     | 
    
         | 
| 
       38 
46 
     | 
    
         
             
              private
         
     | 
| 
         @@ -67,4 +75,5 @@ class Settings 
     | 
|
| 
       67 
75 
     | 
    
         | 
| 
       68 
76 
     | 
    
         
             
              @@settings = read
         
     | 
| 
       69 
77 
     | 
    
         
             
              @@dirty = false
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
       70 
79 
     | 
    
         
             
            end
         
     | 
    
        data/lib/droxi/state.rb
    CHANGED
    
    | 
         @@ -1,32 +1,46 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            require_relative 'settings'
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
      
 3 
     | 
    
         
            +
            # Encapsulates the session state of the client.
         
     | 
| 
       3 
4 
     | 
    
         
             
            class State
         
     | 
| 
       4 
     | 
    
         
            -
              attr_reader :oldpwd, :pwd, :cache
         
     | 
| 
       5 
     | 
    
         
            -
              attr_accessor :local_oldpwd, :exit_requested
         
     | 
| 
       6 
5 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
               
     | 
| 
      
 6 
     | 
    
         
            +
              # +Hash+ of remote file paths to cached file metadata.
         
     | 
| 
      
 7 
     | 
    
         
            +
              attr_reader :cache
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              # The remote working directory path.
         
     | 
| 
      
 10 
     | 
    
         
            +
              attr_reader :pwd
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              # The previous remote working directory path.
         
     | 
| 
      
 13 
     | 
    
         
            +
              attr_reader :oldpwd
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              # The previous local working directory path.
         
     | 
| 
      
 16 
     | 
    
         
            +
              attr_accessor :local_oldpwd
         
     | 
| 
      
 17 
     | 
    
         
            +
              
         
     | 
| 
      
 18 
     | 
    
         
            +
              # +true+ if the client has requested to quit, +false+ otherwise.
         
     | 
| 
      
 19 
     | 
    
         
            +
              attr_accessor :exit_requested
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              # Return a new application state that uses the given client. Starts at the
         
     | 
| 
      
 22 
     | 
    
         
            +
              # Dropbox root and with an empty cache.
         
     | 
| 
      
 23 
     | 
    
         
            +
              def initialize(client)
         
     | 
| 
      
 24 
     | 
    
         
            +
                @cache = {}
         
     | 
| 
      
 25 
     | 
    
         
            +
                @client = client
         
     | 
| 
      
 26 
     | 
    
         
            +
                @exit_requested = false
         
     | 
| 
       8 
27 
     | 
    
         
             
                @pwd = '/'
         
     | 
| 
       9 
28 
     | 
    
         
             
                @oldpwd = Settings[:oldpwd] || '/'
         
     | 
| 
       10 
29 
     | 
    
         
             
                @local_oldpwd = Dir.pwd
         
     | 
| 
       11 
     | 
    
         
            -
                @cache = {}
         
     | 
| 
       12 
     | 
    
         
            -
                @exit_requested = false
         
     | 
| 
       13 
30 
     | 
    
         
             
              end
         
     | 
| 
       14 
31 
     | 
    
         | 
| 
       15 
     | 
    
         
            -
               
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
              end
         
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
              def metadata(client, path)
         
     | 
| 
      
 32 
     | 
    
         
            +
              # Return a +Hash+ of the Dropbox metadata for a file, or +nil+ if the file
         
     | 
| 
      
 33 
     | 
    
         
            +
              # does not exist.
         
     | 
| 
      
 34 
     | 
    
         
            +
              def metadata(path)
         
     | 
| 
       21 
35 
     | 
    
         
             
                tokens = path.split('/').drop(1)
         
     | 
| 
       22 
36 
     | 
    
         | 
| 
       23 
37 
     | 
    
         
             
                for i in 0..tokens.length
         
     | 
| 
       24 
38 
     | 
    
         
             
                  partial_path = '/' + tokens.take(i).join('/')
         
     | 
| 
       25 
39 
     | 
    
         
             
                  unless have_all_info_for(partial_path)
         
     | 
| 
       26 
40 
     | 
    
         
             
                    begin
         
     | 
| 
       27 
     | 
    
         
            -
                      data = @cache[partial_path] = client.metadata(partial_path)
         
     | 
| 
      
 41 
     | 
    
         
            +
                      data = @cache[partial_path] = @client.metadata(partial_path)
         
     | 
| 
       28 
42 
     | 
    
         
             
                    rescue DropboxError
         
     | 
| 
       29 
     | 
    
         
            -
                      return
         
     | 
| 
      
 43 
     | 
    
         
            +
                      return nil
         
     | 
| 
       30 
44 
     | 
    
         
             
                    end
         
     | 
| 
       31 
45 
     | 
    
         
             
                    if data.include?('contents')
         
     | 
| 
       32 
46 
     | 
    
         
             
                      data['contents'].each do |datum|  
         
     | 
| 
         @@ -39,24 +53,30 @@ class State 
     | 
|
| 
       39 
53 
     | 
    
         
             
                @cache[path]
         
     | 
| 
       40 
54 
     | 
    
         
             
              end
         
     | 
| 
       41 
55 
     | 
    
         | 
| 
       42 
     | 
    
         
            -
               
     | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
      
 56 
     | 
    
         
            +
              # Return an +Array+ of paths of files in a Dropbox directory.
         
     | 
| 
      
 57 
     | 
    
         
            +
              def contents(path)
         
     | 
| 
      
 58 
     | 
    
         
            +
                metadata(path)
         
     | 
| 
       44 
59 
     | 
    
         
             
                path = "#{path}/".sub('//', '/')
         
     | 
| 
       45 
60 
     | 
    
         
             
                @cache.keys.select do |key|
         
     | 
| 
       46 
61 
     | 
    
         
             
                  key.start_with?(path) && key != path && !key.sub(path, '').include?('/')
         
     | 
| 
       47 
62 
     | 
    
         
             
                end
         
     | 
| 
       48 
63 
     | 
    
         
             
              end
         
     | 
| 
       49 
64 
     | 
    
         | 
| 
       50 
     | 
    
         
            -
               
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
      
 65 
     | 
    
         
            +
              # Return +true+ if the Dropbox path is a directory, +false+ otherwise.
         
     | 
| 
      
 66 
     | 
    
         
            +
              def directory?(path)
         
     | 
| 
      
 67 
     | 
    
         
            +
                path = path.sub('//', '/')
         
     | 
| 
      
 68 
     | 
    
         
            +
                metadata(File.dirname(path))
         
     | 
| 
       52 
69 
     | 
    
         
             
                @cache.include?(path) && @cache[path]['is_dir']
         
     | 
| 
       53 
70 
     | 
    
         
             
              end
         
     | 
| 
       54 
71 
     | 
    
         | 
| 
      
 72 
     | 
    
         
            +
              # Set the remote working directory, and set the previous remote working
         
     | 
| 
      
 73 
     | 
    
         
            +
              # directory to the old value.
         
     | 
| 
       55 
74 
     | 
    
         
             
              def pwd=(value)
         
     | 
| 
       56 
75 
     | 
    
         
             
                @oldpwd, @pwd = @pwd, value
         
     | 
| 
       57 
76 
     | 
    
         
             
                Settings[:oldpwd] = @oldpwd
         
     | 
| 
       58 
77 
     | 
    
         
             
              end
         
     | 
| 
       59 
78 
     | 
    
         | 
| 
      
 79 
     | 
    
         
            +
              # Expand a Dropbox file path and return the result.
         
     | 
| 
       60 
80 
     | 
    
         
             
              def resolve_path(path)
         
     | 
| 
       61 
81 
     | 
    
         
             
                path = "#{@pwd}/#{path}" unless path.start_with?('/')
         
     | 
| 
       62 
82 
     | 
    
         
             
                path.gsub!('//', '/')
         
     | 
| 
         @@ -67,12 +87,14 @@ class State 
     | 
|
| 
       67 
87 
     | 
    
         
             
                path
         
     | 
| 
       68 
88 
     | 
    
         
             
              end
         
     | 
| 
       69 
89 
     | 
    
         | 
| 
       70 
     | 
    
         
            -
               
     | 
| 
      
 90 
     | 
    
         
            +
              # Expand an +Array+ of file globs into an an +Array+ of Dropbox file paths
         
     | 
| 
      
 91 
     | 
    
         
            +
              # and return the result.
         
     | 
| 
      
 92 
     | 
    
         
            +
              def expand_patterns(patterns)
         
     | 
| 
       71 
93 
     | 
    
         
             
                patterns.map do |pattern|
         
     | 
| 
       72 
94 
     | 
    
         
             
                  final_pattern = resolve_path(pattern)
         
     | 
| 
       73 
95 
     | 
    
         | 
| 
       74 
96 
     | 
    
         
             
                  matches = []
         
     | 
| 
       75 
     | 
    
         
            -
                  client.metadata(File.dirname(final_pattern))['contents'].each do |data|
         
     | 
| 
      
 97 
     | 
    
         
            +
                  @client.metadata(File.dirname(final_pattern))['contents'].each do |data|
         
     | 
| 
       76 
98 
     | 
    
         
             
                    path = data['path']
         
     | 
| 
       77 
99 
     | 
    
         
             
                    matches << path if File.fnmatch(final_pattern, path)
         
     | 
| 
       78 
100 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -85,43 +107,11 @@ class State 
     | 
|
| 
       85 
107 
     | 
    
         
             
                end.flatten
         
     | 
| 
       86 
108 
     | 
    
         
             
              end
         
     | 
| 
       87 
109 
     | 
    
         | 
| 
       88 
     | 
    
         
            -
              def file_complete(client, word)
         
     | 
| 
       89 
     | 
    
         
            -
                tab_complete(client, word, false)
         
     | 
| 
       90 
     | 
    
         
            -
              end
         
     | 
| 
       91 
     | 
    
         
            -
             
     | 
| 
       92 
     | 
    
         
            -
              def dir_complete(client, word)
         
     | 
| 
       93 
     | 
    
         
            -
                tab_complete(client, word, true)
         
     | 
| 
       94 
     | 
    
         
            -
              end
         
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
110 
     | 
    
         
             
              private
         
     | 
| 
       97 
111 
     | 
    
         | 
| 
       98 
     | 
    
         
            -
              def  
     | 
| 
       99 
     | 
    
         
            -
                @cache. 
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
       101 
     | 
    
         
            -
                  !(dir_only && !@cache[key]['is_dir'])
         
     | 
| 
       102 
     | 
    
         
            -
                end.map do |key|
         
     | 
| 
       103 
     | 
    
         
            -
                  if @cache[key]['is_dir']
         
     | 
| 
       104 
     | 
    
         
            -
                    key += '/' 
         
     | 
| 
       105 
     | 
    
         
            -
                  else
         
     | 
| 
       106 
     | 
    
         
            -
                    key += ' '
         
     | 
| 
       107 
     | 
    
         
            -
                  end
         
     | 
| 
       108 
     | 
    
         
            -
                  key[prefix_length, key.length]
         
     | 
| 
       109 
     | 
    
         
            -
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
              def have_all_info_for(path)
         
     | 
| 
      
 113 
     | 
    
         
            +
                @cache.include?(path) &&
         
     | 
| 
      
 114 
     | 
    
         
            +
                (@cache[path].include?('contents') || !@cache[path]['is_dir'])
         
     | 
| 
       110 
115 
     | 
    
         
             
              end
         
     | 
| 
       111 
116 
     | 
    
         | 
| 
       112 
     | 
    
         
            -
              def tab_complete(client, word, dir_only)
         
     | 
| 
       113 
     | 
    
         
            -
                path = resolve_path(word)
         
     | 
| 
       114 
     | 
    
         
            -
                prefix_length = path.length - word.length
         
     | 
| 
       115 
     | 
    
         
            -
             
     | 
| 
       116 
     | 
    
         
            -
                if word.end_with?('/')
         
     | 
| 
       117 
     | 
    
         
            -
                  # Treat word as directory
         
     | 
| 
       118 
     | 
    
         
            -
                  metadata(client, path)
         
     | 
| 
       119 
     | 
    
         
            -
                  prefix_length += 1
         
     | 
| 
       120 
     | 
    
         
            -
                else
         
     | 
| 
       121 
     | 
    
         
            -
                  # Treat word as file
         
     | 
| 
       122 
     | 
    
         
            -
                  metadata(client, File.dirname(path))
         
     | 
| 
       123 
     | 
    
         
            -
                end
         
     | 
| 
       124 
     | 
    
         
            -
             
     | 
| 
       125 
     | 
    
         
            -
                complete(path, prefix_length, dir_only)
         
     | 
| 
       126 
     | 
    
         
            -
              end
         
     | 
| 
       127 
117 
     | 
    
         
             
            end
         
     | 
    
        data/lib/droxi/text.rb
    ADDED
    
    | 
         @@ -0,0 +1,67 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Module containing text-manipulation methods.
         
     | 
| 
      
 2 
     | 
    
         
            +
            module Text
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
              # The assumed width of the terminal if GNU Readline can't retrieve it.
         
     | 
| 
      
 5 
     | 
    
         
            +
              DEFAULT_WIDTH = 72
         
     | 
| 
      
 6 
     | 
    
         
            +
              
         
     | 
| 
      
 7 
     | 
    
         
            +
              # Format an +Array+ of +Strings+ as a table and return an +Array+ of lines
         
     | 
| 
      
 8 
     | 
    
         
            +
              # in the result.
         
     | 
| 
      
 9 
     | 
    
         
            +
              def self.table(items)
         
     | 
| 
      
 10 
     | 
    
         
            +
                if items.empty?
         
     | 
| 
      
 11 
     | 
    
         
            +
                  []
         
     | 
| 
      
 12 
     | 
    
         
            +
                else
         
     | 
| 
      
 13 
     | 
    
         
            +
                  columns = get_columns
         
     | 
| 
      
 14 
     | 
    
         
            +
                  item_width = items.map { |item| item.length }.max + 2
         
     | 
| 
      
 15 
     | 
    
         
            +
                  items_per_line = [1, columns / item_width].max
         
     | 
| 
      
 16 
     | 
    
         
            +
                  num_lines = (items.length.to_f / items_per_line).ceil
         
     | 
| 
      
 17 
     | 
    
         
            +
                  format_table(items, item_width, items_per_line, num_lines)
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              # Wrap a +String+ to fit the terminal and return an +Array+ of lines in the
         
     | 
| 
      
 22 
     | 
    
         
            +
              # result.
         
     | 
| 
      
 23 
     | 
    
         
            +
              def self.wrap(text)
         
     | 
| 
      
 24 
     | 
    
         
            +
                columns = get_columns
         
     | 
| 
      
 25 
     | 
    
         
            +
                position = 0
         
     | 
| 
      
 26 
     | 
    
         
            +
                lines = []
         
     | 
| 
      
 27 
     | 
    
         
            +
                while position < text.length
         
     | 
| 
      
 28 
     | 
    
         
            +
                  lines << get_wrap_segment(text[position, text.length], columns)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  position += lines.last.length + 1
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
                lines
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
              private
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              def self.get_columns
         
     | 
| 
      
 37 
     | 
    
         
            +
                require 'readline'
         
     | 
| 
      
 38 
     | 
    
         
            +
                begin
         
     | 
| 
      
 39 
     | 
    
         
            +
                  columns = Readline.get_screen_size[1]
         
     | 
| 
      
 40 
     | 
    
         
            +
                  columns > 0 ? columns : DEFAULT_WIDTH
         
     | 
| 
      
 41 
     | 
    
         
            +
                rescue NotImplementedError
         
     | 
| 
      
 42 
     | 
    
         
            +
                  DEFAULT_WIDTH
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
              def self.format_table(items, item_width, items_per_line, num_lines)
         
     | 
| 
      
 47 
     | 
    
         
            +
                num_lines.times.map do |i|
         
     | 
| 
      
 48 
     | 
    
         
            +
                  items[i * items_per_line, items_per_line].map do |item|
         
     | 
| 
      
 49 
     | 
    
         
            +
                    item.ljust(item_width)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end.join
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
              end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
              def self.get_wrap_segment(text, columns)
         
     | 
| 
      
 55 
     | 
    
         
            +
                segment, sep, text = text.partition(' ')
         
     | 
| 
      
 56 
     | 
    
         
            +
                while !text.empty? && segment.length < columns
         
     | 
| 
      
 57 
     | 
    
         
            +
                  head, sep, text = text.partition(' ')
         
     | 
| 
      
 58 
     | 
    
         
            +
                  segment << " #{head}"
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
                if segment.length > columns && segment.include?(' ')
         
     | 
| 
      
 61 
     | 
    
         
            +
                  segment.rpartition(' ')[0]
         
     | 
| 
      
 62 
     | 
    
         
            +
                else
         
     | 
| 
      
 63 
     | 
    
         
            +
                  segment
         
     | 
| 
      
 64 
     | 
    
         
            +
                end
         
     | 
| 
      
 65 
     | 
    
         
            +
              end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/droxi.rb
    CHANGED
    
    | 
         @@ -2,15 +2,19 @@ require 'dropbox_sdk' 
     | 
|
| 
       2 
2 
     | 
    
         
             
            require 'readline'
         
     | 
| 
       3 
3 
     | 
    
         | 
| 
       4 
4 
     | 
    
         
             
            require_relative 'droxi/commands'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative 'droxi/complete'
         
     | 
| 
       5 
6 
     | 
    
         
             
            require_relative 'droxi/settings'
         
     | 
| 
       6 
7 
     | 
    
         
             
            require_relative 'droxi/state'
         
     | 
| 
       7 
8 
     | 
    
         | 
| 
      
 9 
     | 
    
         
            +
            # Command-line Dropbox client module.
         
     | 
| 
       8 
10 
     | 
    
         
             
            module Droxi
         
     | 
| 
       9 
     | 
    
         
            -
              APP_KEY = '5sufyfrvtro9zp7'
         
     | 
| 
       10 
     | 
    
         
            -
              APP_SECRET = 'h99ihzv86jyypho'
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
      
 12 
     | 
    
         
            +
              # Attempt to authorize the user for app usage.
         
     | 
| 
       12 
13 
     | 
    
         
             
              def self.authorize
         
     | 
| 
       13 
     | 
    
         
            -
                 
     | 
| 
      
 14 
     | 
    
         
            +
                app_key = '5sufyfrvtro9zp7'
         
     | 
| 
      
 15 
     | 
    
         
            +
                app_secret = 'h99ihzv86jyypho'
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                flow = DropboxOAuth2FlowNoRedirect.new(app_key, app_secret)
         
     | 
| 
       14 
18 
     | 
    
         | 
| 
       15 
19 
     | 
    
         
             
                authorize_url = flow.start()
         
     | 
| 
       16 
20 
     | 
    
         | 
| 
         @@ -34,8 +38,12 @@ module Droxi 
     | 
|
| 
       34 
38 
     | 
    
         
             
                rescue DropboxError
         
     | 
| 
       35 
39 
     | 
    
         
             
                  puts 'Invalid authorization code.'
         
     | 
| 
       36 
40 
     | 
    
         
             
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                nil
         
     | 
| 
       37 
43 
     | 
    
         
             
              end
         
     | 
| 
       38 
44 
     | 
    
         | 
| 
      
 45 
     | 
    
         
            +
              # Get the access token for the user, requesting authorization if no token
         
     | 
| 
      
 46 
     | 
    
         
            +
              # exists.
         
     | 
| 
       39 
47 
     | 
    
         
             
              def self.get_access_token
         
     | 
| 
       40 
48 
     | 
    
         
             
                until Settings.include?(:access_token)
         
     | 
| 
       41 
49 
     | 
    
         
             
                  authorize()
         
     | 
| 
         @@ -43,52 +51,16 @@ module Droxi 
     | 
|
| 
       43 
51 
     | 
    
         
             
                Settings[:access_token]
         
     | 
| 
       44 
52 
     | 
    
         
             
              end
         
     | 
| 
       45 
53 
     | 
    
         | 
| 
      
 54 
     | 
    
         
            +
              # Print a prompt message reflecting the current state of the application.
         
     | 
| 
       46 
55 
     | 
    
         
             
              def self.prompt(info, state)
         
     | 
| 
       47 
56 
     | 
    
         
             
                "droxi #{info['email']}:#{state.pwd}> "
         
     | 
| 
       48 
57 
     | 
    
         
             
              end
         
     | 
| 
       49 
58 
     | 
    
         | 
| 
       50 
     | 
    
         
            -
               
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
                  path = File.expand_path(word)
         
     | 
| 
       53 
     | 
    
         
            -
                rescue ArgumentError
         
     | 
| 
       54 
     | 
    
         
            -
                  return []
         
     | 
| 
       55 
     | 
    
         
            -
                end
         
     | 
| 
       56 
     | 
    
         
            -
                if word.empty? || (word.length > 1 && word.end_with?('/'))
         
     | 
| 
       57 
     | 
    
         
            -
                  dir = path
         
     | 
| 
       58 
     | 
    
         
            -
                else
         
     | 
| 
       59 
     | 
    
         
            -
                  dir = File.dirname(path)
         
     | 
| 
       60 
     | 
    
         
            -
                end
         
     | 
| 
       61 
     | 
    
         
            -
                Dir.entries(dir).map do |file|
         
     | 
| 
       62 
     | 
    
         
            -
                  (dir + '/').sub('//', '/') + file
         
     | 
| 
       63 
     | 
    
         
            -
                end.select do |file|
         
     | 
| 
       64 
     | 
    
         
            -
                  file.start_with?(path) && !(dir_only && !File.directory?(file))
         
     | 
| 
       65 
     | 
    
         
            -
                end.map do |file|
         
     | 
| 
       66 
     | 
    
         
            -
                  if File.directory?(file)
         
     | 
| 
       67 
     | 
    
         
            -
                    file << '/'
         
     | 
| 
       68 
     | 
    
         
            -
                  else
         
     | 
| 
       69 
     | 
    
         
            -
                    file << ' '
         
     | 
| 
       70 
     | 
    
         
            -
                  end
         
     | 
| 
       71 
     | 
    
         
            -
                  if word.start_with?('/')
         
     | 
| 
       72 
     | 
    
         
            -
                    file
         
     | 
| 
       73 
     | 
    
         
            -
                  elsif word.start_with?('~')
         
     | 
| 
       74 
     | 
    
         
            -
                    file.sub(/\/home\/[^\/]+/, '~')
         
     | 
| 
       75 
     | 
    
         
            -
                  else
         
     | 
| 
       76 
     | 
    
         
            -
                    file.sub(Dir.pwd + '/', '')
         
     | 
| 
       77 
     | 
    
         
            -
                  end
         
     | 
| 
       78 
     | 
    
         
            -
                end
         
     | 
| 
       79 
     | 
    
         
            -
              end
         
     | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
     | 
    
         
            -
              def self.dir_complete(word)
         
     | 
| 
       82 
     | 
    
         
            -
                file_complete(word, true)
         
     | 
| 
       83 
     | 
    
         
            -
              end
         
     | 
| 
       84 
     | 
    
         
            -
             
     | 
| 
       85 
     | 
    
         
            -
              def self.run
         
     | 
| 
       86 
     | 
    
         
            -
                client = DropboxClient.new(get_access_token)
         
     | 
| 
      
 59 
     | 
    
         
            +
              # Run the client in interactive mode.
         
     | 
| 
      
 60 
     | 
    
         
            +
              def self.run_interactive(client, state)
         
     | 
| 
       87 
61 
     | 
    
         
             
                info = client.account_info
         
     | 
| 
       88 
62 
     | 
    
         
             
                puts "Logged in as #{info['display_name']} (#{info['email']})"
         
     | 
| 
       89 
63 
     | 
    
         | 
| 
       90 
     | 
    
         
            -
                state = State.new
         
     | 
| 
       91 
     | 
    
         
            -
             
     | 
| 
       92 
64 
     | 
    
         
             
                Readline.completion_proc = proc do |word|
         
     | 
| 
       93 
65 
     | 
    
         
             
                  words = Readline.line_buffer.split
         
     | 
| 
       94 
66 
     | 
    
         
             
                  index = words.length
         
     | 
| 
         @@ -105,24 +77,11 @@ module Droxi 
     | 
|
| 
       105 
77 
     | 
    
         
             
                    Commands::NAMES.select { |name| name.start_with? word }.map do |name|
         
     | 
| 
       106 
78 
     | 
    
         
             
                      name + ' '
         
     | 
| 
       107 
79 
     | 
    
         
             
                    end
         
     | 
| 
       108 
     | 
    
         
            -
                  when 'LOCAL_FILE'
         
     | 
| 
       109 
     | 
    
         
            -
             
     | 
| 
       110 
     | 
    
         
            -
                  when ' 
     | 
| 
       111 
     | 
    
         
            -
             
     | 
| 
       112 
     | 
    
         
            -
                   
     | 
| 
       113 
     | 
    
         
            -
                    begin
         
     | 
| 
       114 
     | 
    
         
            -
                      state.file_complete(client, word)
         
     | 
| 
       115 
     | 
    
         
            -
                    rescue DropboxError
         
     | 
| 
       116 
     | 
    
         
            -
                      []
         
     | 
| 
       117 
     | 
    
         
            -
                    end
         
     | 
| 
       118 
     | 
    
         
            -
                  when 'REMOTE_DIR'
         
     | 
| 
       119 
     | 
    
         
            -
                    begin
         
     | 
| 
       120 
     | 
    
         
            -
                      state.dir_complete(client, word)
         
     | 
| 
       121 
     | 
    
         
            -
                    rescue DropboxError
         
     | 
| 
       122 
     | 
    
         
            -
                      []
         
     | 
| 
       123 
     | 
    
         
            -
                    end
         
     | 
| 
       124 
     | 
    
         
            -
                  else
         
     | 
| 
       125 
     | 
    
         
            -
                    []
         
     | 
| 
      
 80 
     | 
    
         
            +
                  when 'LOCAL_FILE'  then Complete.local(word)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  when 'LOCAL_DIR'   then Complete.local_dir(word)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  when 'REMOTE_FILE' then Complete.remote(word, state)
         
     | 
| 
      
 83 
     | 
    
         
            +
                  when 'REMOTE_DIR'  then Complete.remote_dir(word, state)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  else []
         
     | 
| 
       126 
85 
     | 
    
         
             
                  end
         
     | 
| 
       127 
86 
     | 
    
         | 
| 
       128 
87 
     | 
    
         
             
                  options.map { |option| option.gsub(' ', '\ ').sub(/\\ $/, ' ') }
         
     | 
| 
         @@ -143,7 +102,26 @@ module Droxi 
     | 
|
| 
       143 
102 
     | 
    
         
             
                  puts
         
     | 
| 
       144 
103 
     | 
    
         
             
                end
         
     | 
| 
       145 
104 
     | 
    
         | 
| 
      
 105 
     | 
    
         
            +
                # Set pwd so that the oldpwd setting is set to pwd
         
     | 
| 
       146 
106 
     | 
    
         
             
                state.pwd = '/'
         
     | 
| 
       147 
     | 
    
         
            -
                Settings. 
     | 
| 
      
 107 
     | 
    
         
            +
                Settings.save
         
     | 
| 
      
 108 
     | 
    
         
            +
              end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
              # Run the client.
         
     | 
| 
      
 111 
     | 
    
         
            +
              def self.run(*args)
         
     | 
| 
      
 112 
     | 
    
         
            +
                client = DropboxClient.new(get_access_token)
         
     | 
| 
      
 113 
     | 
    
         
            +
                state = State.new(client)
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                if args.empty?
         
     | 
| 
      
 116 
     | 
    
         
            +
                  run_interactive(client, state)
         
     | 
| 
      
 117 
     | 
    
         
            +
                else
         
     | 
| 
      
 118 
     | 
    
         
            +
                  cmd = args.map { |arg| arg.gsub(' ', '\ ') }.join(' ')
         
     | 
| 
      
 119 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 120 
     | 
    
         
            +
                    Commands.exec(cmd, client, state)
         
     | 
| 
      
 121 
     | 
    
         
            +
                  rescue Interrupt
         
     | 
| 
      
 122 
     | 
    
         
            +
                    puts
         
     | 
| 
      
 123 
     | 
    
         
            +
                  end
         
     | 
| 
      
 124 
     | 
    
         
            +
                end
         
     | 
| 
       148 
125 
     | 
    
         
             
              end
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
       149 
127 
     | 
    
         
             
            end
         
     | 
    
        data/spec/commands_spec.rb
    CHANGED
    
    | 
         @@ -35,7 +35,7 @@ describe Commands do 
     | 
|
| 
       35 
35 
     | 
    
         
             
              original_dir = Dir.pwd
         
     | 
| 
       36 
36 
     | 
    
         | 
| 
       37 
37 
     | 
    
         
             
              client = DropboxClient.new(Settings[:access_token])
         
     | 
| 
       38 
     | 
    
         
            -
              state = State.new
         
     | 
| 
      
 38 
     | 
    
         
            +
              state = State.new(client)
         
     | 
| 
       39 
39 
     | 
    
         | 
| 
       40 
40 
     | 
    
         
             
              TEMP_FILENAME = 'test.txt'
         
     | 
| 
       41 
41 
     | 
    
         
             
              TEMP_FOLDER = 'test'
         
     | 
| 
         @@ -44,6 +44,10 @@ describe Commands do 
     | 
|
| 
       44 
44 
     | 
    
         
             
              ignore(DropboxError) { client.file_delete("/#{TEST_FOLDER}") }
         
     | 
| 
       45 
45 
     | 
    
         
             
              ignore(DropboxError) { client.file_create_folder("/#{TEST_FOLDER}") }
         
     | 
| 
       46 
46 
     | 
    
         | 
| 
      
 47 
     | 
    
         
            +
              before do
         
     | 
| 
      
 48 
     | 
    
         
            +
                Dir.chdir(original_dir)
         
     | 
| 
      
 49 
     | 
    
         
            +
              end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
       47 
51 
     | 
    
         
             
              describe 'when executing a shell command' do
         
     | 
| 
       48 
52 
     | 
    
         
             
                it 'must yield the output' do
         
     | 
| 
       49 
53 
     | 
    
         
             
                  lines = []
         
     | 
| 
         @@ -88,7 +92,6 @@ describe Commands do 
     | 
|
| 
       88 
92 
     | 
    
         | 
| 
       89 
93 
     | 
    
         
             
              describe 'when executing the get command' do
         
     | 
| 
       90 
94 
     | 
    
         
             
                it 'must get a file of the same name when given args' do
         
     | 
| 
       91 
     | 
    
         
            -
                  Dir.chdir(original_dir)
         
     | 
| 
       92 
95 
     | 
    
         
             
                  put_temp_file(client, state)
         
     | 
| 
       93 
96 
     | 
    
         
             
                  Commands::GET.exec(client, state, '/testing/test.txt')
         
     | 
| 
       94 
97 
     | 
    
         
             
                  delete_temp_file(client, state)
         
     | 
| 
         @@ -99,26 +102,22 @@ describe Commands do 
     | 
|
| 
       99 
102 
     | 
    
         | 
| 
       100 
103 
     | 
    
         
             
              describe 'when executing the lcd command' do
         
     | 
| 
       101 
104 
     | 
    
         
             
                it 'must change to home directory when given no args' do
         
     | 
| 
       102 
     | 
    
         
            -
                  Dir.chdir(original_dir)
         
     | 
| 
       103 
105 
     | 
    
         
             
                  Commands::LCD.exec(client, state)
         
     | 
| 
       104 
106 
     | 
    
         
             
                  Dir.pwd.must_equal File.expand_path('~')
         
     | 
| 
       105 
107 
     | 
    
         
             
                end
         
     | 
| 
       106 
108 
     | 
    
         | 
| 
       107 
109 
     | 
    
         
             
                it 'must change to specific directory when specified' do
         
     | 
| 
       108 
     | 
    
         
            -
                  Dir.chdir(original_dir)
         
     | 
| 
       109 
110 
     | 
    
         
             
                  Commands::LCD.exec(client, state, '/home')
         
     | 
| 
       110 
111 
     | 
    
         
             
                  Dir.pwd.must_equal File.expand_path('/home')
         
     | 
| 
       111 
112 
     | 
    
         
             
                end
         
     | 
| 
       112 
113 
     | 
    
         | 
| 
       113 
114 
     | 
    
         
             
                it 'must set oldpwd correctly' do
         
     | 
| 
       114 
     | 
    
         
            -
                  Dir.chdir(original_dir)
         
     | 
| 
       115 
115 
     | 
    
         
             
                  oldpwd = Dir.pwd
         
     | 
| 
       116 
116 
     | 
    
         
             
                  Commands::LCD.exec(client, state, '/')
         
     | 
| 
       117 
117 
     | 
    
         
             
                  state.local_oldpwd.must_equal oldpwd
         
     | 
| 
       118 
118 
     | 
    
         
             
                end
         
     | 
| 
       119 
119 
     | 
    
         | 
| 
       120 
120 
     | 
    
         
             
                it 'must change to previous directory when given -' do
         
     | 
| 
       121 
     | 
    
         
            -
                  Dir.chdir(original_dir)
         
     | 
| 
       122 
121 
     | 
    
         
             
                  oldpwd = Dir.pwd
         
     | 
| 
       123 
122 
     | 
    
         
             
                  Commands::LCD.exec(client, state, '/')
         
     | 
| 
       124 
123 
     | 
    
         
             
                  Commands::LCD.exec(client, state, '-')
         
     | 
| 
         @@ -126,7 +125,6 @@ describe Commands do 
     | 
|
| 
       126 
125 
     | 
    
         
             
                end
         
     | 
| 
       127 
126 
     | 
    
         | 
| 
       128 
127 
     | 
    
         
             
                it 'must fail if given bogus directory name' do
         
     | 
| 
       129 
     | 
    
         
            -
                  Dir.chdir(original_dir)
         
     | 
| 
       130 
128 
     | 
    
         
             
                  pwd = Dir.pwd
         
     | 
| 
       131 
129 
     | 
    
         
             
                  oldpwd = state.local_oldpwd
         
     | 
| 
       132 
130 
     | 
    
         
             
                  Commands::LCD.exec(client, state, '/bogus_dir')
         
     | 
| 
         @@ -155,6 +153,17 @@ describe Commands do 
     | 
|
| 
       155 
153 
     | 
    
         
             
                end
         
     | 
| 
       156 
154 
     | 
    
         
             
              end
         
     | 
| 
       157 
155 
     | 
    
         | 
| 
      
 156 
     | 
    
         
            +
              describe 'when executing the media command' do
         
     | 
| 
      
 157 
     | 
    
         
            +
                it 'must yield URL when given file path' do
         
     | 
| 
      
 158 
     | 
    
         
            +
                  put_temp_file(client, state)
         
     | 
| 
      
 159 
     | 
    
         
            +
                  to_path = "/#{TEST_FOLDER}/#{TEMP_FILENAME}"
         
     | 
| 
      
 160 
     | 
    
         
            +
                  lines = get_output(:MEDIA, client, state, to_path)
         
     | 
| 
      
 161 
     | 
    
         
            +
                  delete_temp_file(client, state)
         
     | 
| 
      
 162 
     | 
    
         
            +
                  lines.length.must_equal 1
         
     | 
| 
      
 163 
     | 
    
         
            +
                  /https:\/\/.+\..+\//.match(lines[0]).wont_equal nil
         
     | 
| 
      
 164 
     | 
    
         
            +
                end
         
     | 
| 
      
 165 
     | 
    
         
            +
              end
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
       158 
167 
     | 
    
         
             
              describe 'when executing the mkdir command' do
         
     | 
| 
       159 
168 
     | 
    
         
             
                it 'must create a directory when given args' do
         
     | 
| 
       160 
169 
     | 
    
         
             
                  Commands::MKDIR.exec(client, state, '/testing/test')
         
     | 
| 
         @@ -165,7 +174,6 @@ describe Commands do 
     | 
|
| 
       165 
174 
     | 
    
         | 
| 
       166 
175 
     | 
    
         
             
              describe 'when executing the put command' do
         
     | 
| 
       167 
176 
     | 
    
         
             
                it 'must put a file of the same name when given 1 arg' do
         
     | 
| 
       168 
     | 
    
         
            -
                  Dir.chdir(original_dir)
         
     | 
| 
       169 
177 
     | 
    
         
             
                  state.pwd = '/testing'
         
     | 
| 
       170 
178 
     | 
    
         
             
                  `echo hello > test.txt`
         
     | 
| 
       171 
179 
     | 
    
         
             
                  Commands::PUT.exec(client, state, 'test.txt')
         
     | 
| 
         @@ -175,7 +183,6 @@ describe Commands do 
     | 
|
| 
       175 
183 
     | 
    
         
             
                end
         
     | 
| 
       176 
184 
     | 
    
         | 
| 
       177 
185 
     | 
    
         
             
                it 'must put a file with the stated name when given 2 args' do
         
     | 
| 
       178 
     | 
    
         
            -
                  Dir.chdir(original_dir)
         
     | 
| 
       179 
186 
     | 
    
         
             
                  state.pwd = '/testing'
         
     | 
| 
       180 
187 
     | 
    
         
             
                  `echo hello > test.txt`
         
     | 
| 
       181 
188 
     | 
    
         
             
                  Commands::PUT.exec(client, state, 'test.txt', 'dest.txt')
         
     | 
| 
         @@ -0,0 +1,114 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'dropbox_sdk'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'minitest/autorun'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative '../lib/droxi/complete'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative '../lib/droxi/settings'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative '../lib/droxi/state'
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            describe Complete do
         
     | 
| 
      
 9 
     | 
    
         
            +
              CHARACTERS = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              def random_character
         
     | 
| 
      
 12 
     | 
    
         
            +
                CHARACTERS[rand(CHARACTERS.length)]
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              def random_string(length)
         
     | 
| 
      
 16 
     | 
    
         
            +
                rand(length).times.map { random_character }.join
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
              describe "when resolving a local search path" do
         
     | 
| 
      
 20 
     | 
    
         
            +
                it "must resolve unqualified string to working directory" do
         
     | 
| 
      
 21 
     | 
    
         
            +
                  Complete.local_search_path('').must_equal Dir.pwd
         
     | 
| 
      
 22 
     | 
    
         
            +
                  Complete.local_search_path('f').must_equal Dir.pwd
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                it "must resolve / to root directory" do
         
     | 
| 
      
 26 
     | 
    
         
            +
                  Complete.local_search_path('/').must_equal '/'
         
     | 
| 
      
 27 
     | 
    
         
            +
                  Complete.local_search_path('/f').must_equal '/'
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                it "must resolve directory name to named directory" do
         
     | 
| 
      
 31 
     | 
    
         
            +
                  Complete.local_search_path('/home/').must_equal '/home'
         
     | 
| 
      
 32 
     | 
    
         
            +
                  Complete.local_search_path('/home/f').must_equal '/home'
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                it "must resolve ~/ to home directory" do
         
     | 
| 
      
 36 
     | 
    
         
            +
                  Complete.local_search_path('~/').must_equal Dir.home
         
     | 
| 
      
 37 
     | 
    
         
            +
                  Complete.local_search_path('~/f').must_equal Dir.home
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                it "must resolve ./ to working directory" do
         
     | 
| 
      
 41 
     | 
    
         
            +
                  Complete.local_search_path('./').must_equal Dir.pwd
         
     | 
| 
      
 42 
     | 
    
         
            +
                  Complete.local_search_path('./f').must_equal Dir.pwd
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                it "must resolve ../ to parent directory" do
         
     | 
| 
      
 46 
     | 
    
         
            +
                  Complete.local_search_path('../').must_equal File.dirname(Dir.pwd)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  Complete.local_search_path('../f').must_equal File.dirname(Dir.pwd)
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                it "won't raise an exception on a bogus string" do
         
     | 
| 
      
 51 
     | 
    
         
            +
                  Complete.local_search_path('~bogus')
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
              describe "when finding potential local tab completions" do
         
     | 
| 
      
 56 
     | 
    
         
            +
                def check(path)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  100.times.all? do |i|
         
     | 
| 
      
 58 
     | 
    
         
            +
                    prefix = path + random_string(5)
         
     | 
| 
      
 59 
     | 
    
         
            +
                    Complete.local(prefix).all? { |match| match.start_with?(prefix) }
         
     | 
| 
      
 60 
     | 
    
         
            +
                  end.must_equal true
         
     | 
| 
      
 61 
     | 
    
         
            +
                  1000.times.any? do |i|
         
     | 
| 
      
 62 
     | 
    
         
            +
                    prefix = path + random_string(5)
         
     | 
| 
      
 63 
     | 
    
         
            +
                    !Complete.local(prefix).empty?
         
     | 
| 
      
 64 
     | 
    
         
            +
                  end.must_equal true
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                it "seed must prefix results for unqualified string" do check('') end
         
     | 
| 
      
 68 
     | 
    
         
            +
                it "seed must prefix results for /" do check('/') end
         
     | 
| 
      
 69 
     | 
    
         
            +
                it "seed must prefix results for named directory" do check('/home/') end
         
     | 
| 
      
 70 
     | 
    
         
            +
                it "seed must prefix results for ~/" do check('~/') end
         
     | 
| 
      
 71 
     | 
    
         
            +
                it "seed must prefix results for ./" do check('./') end
         
     | 
| 
      
 72 
     | 
    
         
            +
                it "seed must prefix results for ../" do check('../') end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                it "won't raise an exception on a bogus string" do
         
     | 
| 
      
 75 
     | 
    
         
            +
                  Complete.local('~bogus')
         
     | 
| 
      
 76 
     | 
    
         
            +
                end
         
     | 
| 
      
 77 
     | 
    
         
            +
              end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
              describe "when resolving a remote search path" do
         
     | 
| 
      
 80 
     | 
    
         
            +
                client = DropboxClient.new(Settings[:access_token])
         
     | 
| 
      
 81 
     | 
    
         
            +
                begin
         
     | 
| 
      
 82 
     | 
    
         
            +
                  client.file_create_folder('/testing')
         
     | 
| 
      
 83 
     | 
    
         
            +
                rescue DropboxError
         
     | 
| 
      
 84 
     | 
    
         
            +
                end
         
     | 
| 
      
 85 
     | 
    
         
            +
                state = State.new(client)
         
     | 
| 
      
 86 
     | 
    
         
            +
                state.pwd = '/testing'
         
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                it "must resolve unqualified string to working directory" do
         
     | 
| 
      
 89 
     | 
    
         
            +
                  Complete.remote_search_path('', state).must_equal state.pwd
         
     | 
| 
      
 90 
     | 
    
         
            +
                  Complete.remote_search_path('f', state).must_equal state.pwd
         
     | 
| 
      
 91 
     | 
    
         
            +
                end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                it "must resolve / to root directory" do
         
     | 
| 
      
 94 
     | 
    
         
            +
                  Complete.remote_search_path('/', state).must_equal '/'
         
     | 
| 
      
 95 
     | 
    
         
            +
                  Complete.remote_search_path('/f', state).must_equal '/'
         
     | 
| 
      
 96 
     | 
    
         
            +
                end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                it "must resolve directory name to named directory" do
         
     | 
| 
      
 99 
     | 
    
         
            +
                  Complete.remote_search_path('/testing/', state).must_equal '/testing'
         
     | 
| 
      
 100 
     | 
    
         
            +
                  Complete.remote_search_path('/testing/f', state).must_equal '/testing'
         
     | 
| 
      
 101 
     | 
    
         
            +
                end
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                it "must resolve ./ to working directory" do
         
     | 
| 
      
 104 
     | 
    
         
            +
                  Complete.remote_search_path('./', state).must_equal state.pwd
         
     | 
| 
      
 105 
     | 
    
         
            +
                  Complete.remote_search_path('./f', state).must_equal state.pwd
         
     | 
| 
      
 106 
     | 
    
         
            +
                end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                it "must resolve ../ to parent directory" do
         
     | 
| 
      
 109 
     | 
    
         
            +
                  parent = File.dirname(state.pwd)
         
     | 
| 
      
 110 
     | 
    
         
            +
                  Complete.remote_search_path('../', state).must_equal parent
         
     | 
| 
      
 111 
     | 
    
         
            +
                  Complete.remote_search_path('../f', state).must_equal parent
         
     | 
| 
      
 112 
     | 
    
         
            +
                end
         
     | 
| 
      
 113 
     | 
    
         
            +
              end
         
     | 
| 
      
 114 
     | 
    
         
            +
            end
         
     | 
    
        data/spec/state_spec.rb
    CHANGED
    
    | 
         @@ -6,19 +6,19 @@ require_relative '../lib/droxi/settings' 
     | 
|
| 
       6 
6 
     | 
    
         
             
            describe State do
         
     | 
| 
       7 
7 
     | 
    
         
             
              describe 'when initializing' do
         
     | 
| 
       8 
8 
     | 
    
         
             
                it 'must set pwd to root' do
         
     | 
| 
       9 
     | 
    
         
            -
                  State.new.pwd.must_equal '/'
         
     | 
| 
      
 9 
     | 
    
         
            +
                  State.new(nil).pwd.must_equal '/'
         
     | 
| 
       10 
10 
     | 
    
         
             
                end
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
12 
     | 
    
         
             
                it 'must set oldpwd to saved oldpwd' do
         
     | 
| 
       13 
13 
     | 
    
         
             
                  if Settings.include?(:oldpwd)
         
     | 
| 
       14 
     | 
    
         
            -
                    State.new.oldpwd.must_equal Settings[:oldpwd]
         
     | 
| 
      
 14 
     | 
    
         
            +
                    State.new(nil).oldpwd.must_equal Settings[:oldpwd]
         
     | 
| 
       15 
15 
     | 
    
         
             
                  end
         
     | 
| 
       16 
16 
     | 
    
         
             
                end
         
     | 
| 
       17 
17 
     | 
    
         
             
              end
         
     | 
| 
       18 
18 
     | 
    
         | 
| 
       19 
19 
     | 
    
         
             
              describe 'when setting pwd' do
         
     | 
| 
       20 
20 
     | 
    
         
             
                it 'must change pwd and set oldpwd to previous pwd' do
         
     | 
| 
       21 
     | 
    
         
            -
                  state = State.new
         
     | 
| 
      
 21 
     | 
    
         
            +
                  state = State.new(nil)
         
     | 
| 
       22 
22 
     | 
    
         
             
                  state.pwd = '/testing'
         
     | 
| 
       23 
23 
     | 
    
         
             
                  state.pwd.must_equal '/testing'
         
     | 
| 
       24 
24 
     | 
    
         
             
                  state.pwd = '/'
         
     | 
| 
         @@ -27,7 +27,7 @@ describe State do 
     | 
|
| 
       27 
27 
     | 
    
         
             
              end
         
     | 
| 
       28 
28 
     | 
    
         | 
| 
       29 
29 
     | 
    
         
             
              describe 'when resolving path' do
         
     | 
| 
       30 
     | 
    
         
            -
                state = State.new
         
     | 
| 
      
 30 
     | 
    
         
            +
                state = State.new(nil)
         
     | 
| 
       31 
31 
     | 
    
         | 
| 
       32 
32 
     | 
    
         
             
                it 'must resolve root to itself' do
         
     | 
| 
       33 
33 
     | 
    
         
             
                  state.resolve_path('/').must_equal '/'
         
     | 
    
        data/spec/text_spec.rb
    ADDED
    
    | 
         @@ -0,0 +1,60 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'minitest/autorun'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative '../lib/droxi/text'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            describe Text do
         
     | 
| 
      
 6 
     | 
    
         
            +
              before do
         
     | 
| 
      
 7 
     | 
    
         
            +
                @columns = Text::DEFAULT_WIDTH
         
     | 
| 
      
 8 
     | 
    
         
            +
                @paragraph = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, \
         
     | 
| 
      
 9 
     | 
    
         
            +
                              sed do eiusmod tempor incididunt ut labore et dolore \
         
     | 
| 
      
 10 
     | 
    
         
            +
                              magna aliqua. Ut enim ad minim veniam, quis nostrud \
         
     | 
| 
      
 11 
     | 
    
         
            +
                              exercitation ullamco laboris nisi ut aliquip ex ea \
         
     | 
| 
      
 12 
     | 
    
         
            +
                              commodo consequat. Duis aute irure dolor in reprehenderit \
         
     | 
| 
      
 13 
     | 
    
         
            +
                              in voluptate velit esse cillum dolore eu fugiat nulla \
         
     | 
| 
      
 14 
     | 
    
         
            +
                              pariatur. Excepteur sint occaecat cupidatat non proident, \
         
     | 
| 
      
 15 
     | 
    
         
            +
                              sunt in culpa qui officia deserunt mollit anim id est \
         
     | 
| 
      
 16 
     | 
    
         
            +
                              laborum.".squeeze(' ')
         
     | 
| 
      
 17 
     | 
    
         
            +
                @big_word = "Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphi\
         
     | 
| 
      
 18 
     | 
    
         
            +
                             oparaomelitokatakechymenokichlepikossyphophattoperisteralektr\
         
     | 
| 
      
 19 
     | 
    
         
            +
                             yonoptekephalliokigklopeleiolagoiosiraiobaphetraganopterygon".
         
     | 
| 
      
 20 
     | 
    
         
            +
                               gsub(' ', '')
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              describe "when wrapping text" do
         
     | 
| 
      
 24 
     | 
    
         
            +
                it "won't return any line larger than the screen width if unnecessary" do
         
     | 
| 
      
 25 
     | 
    
         
            +
                  Text.wrap(@paragraph).all? do |line|
         
     | 
| 
      
 26 
     | 
    
         
            +
                    line.length <= @columns
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end.must_equal true
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                it "won't split a word larger than the screen width" do
         
     | 
| 
      
 31 
     | 
    
         
            +
                  Text.wrap(@big_word).length.must_equal 1
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              describe "when tabulating text" do
         
     | 
| 
      
 36 
     | 
    
         
            +
                it "must space items equally" do
         
     | 
| 
      
 37 
     | 
    
         
            +
                  lines = Text.table(@paragraph.split)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  lines = lines[0, lines.length - 1]
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  space_positions = [0]
         
     | 
| 
      
 41 
     | 
    
         
            +
                  while lines.first.index(/  \S/, space_positions.last + 3)
         
     | 
| 
      
 42 
     | 
    
         
            +
                    space_positions << lines.first.index(/  \S/, space_positions.last + 3)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  space_positions.drop(1).all? do |position|
         
     | 
| 
      
 46 
     | 
    
         
            +
                    lines.all? { |line| /  \S/.match(line[position, 3]) }
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end.must_equal true
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                it "won't return any line larger than the screen width if unnecessary" do
         
     | 
| 
      
 51 
     | 
    
         
            +
                  Text.table(@paragraph.split).all? do |line|
         
     | 
| 
      
 52 
     | 
    
         
            +
                    line.length <= @columns
         
     | 
| 
      
 53 
     | 
    
         
            +
                  end.must_equal true
         
     | 
| 
      
 54 
     | 
    
         
            +
                end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                it "won't split a word larger than the screen width" do
         
     | 
| 
      
 57 
     | 
    
         
            +
                  Text.table([@big_word]).length.must_equal 1
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
              end
         
     | 
| 
      
 60 
     | 
    
         
            +
            end
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: droxi
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0.0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.0.2
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Brandon Mulcahy
         
     | 
| 
         @@ -30,8 +30,8 @@ dependencies: 
     | 
|
| 
       30 
30 
     | 
    
         
             
                - - ">="
         
     | 
| 
       31 
31 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       32 
32 
     | 
    
         
             
                    version: 1.6.1
         
     | 
| 
       33 
     | 
    
         
            -
            description:  
     | 
| 
       34 
     | 
    
         
            -
              and lftp.  
     | 
| 
      
 33 
     | 
    
         
            +
            description: A command-line Dropbox interface inspired by GNU coreutils, GNU ftp,
         
     | 
| 
      
 34 
     | 
    
         
            +
              and lftp. Features include smart tab completion, globbing, and interactive help.
         
     | 
| 
       35 
35 
     | 
    
         
             
            email: brandon@jangler.info
         
     | 
| 
       36 
36 
     | 
    
         
             
            executables:
         
     | 
| 
       37 
37 
     | 
    
         
             
            - droxi
         
     | 
| 
         @@ -46,12 +46,16 @@ files: 
     | 
|
| 
       46 
46 
     | 
    
         
             
            - droxi.gemspec
         
     | 
| 
       47 
47 
     | 
    
         
             
            - lib/droxi.rb
         
     | 
| 
       48 
48 
     | 
    
         
             
            - lib/droxi/commands.rb
         
     | 
| 
      
 49 
     | 
    
         
            +
            - lib/droxi/complete.rb
         
     | 
| 
       49 
50 
     | 
    
         
             
            - lib/droxi/settings.rb
         
     | 
| 
       50 
51 
     | 
    
         
             
            - lib/droxi/state.rb
         
     | 
| 
      
 52 
     | 
    
         
            +
            - lib/droxi/text.rb
         
     | 
| 
       51 
53 
     | 
    
         
             
            - spec/all.rb
         
     | 
| 
       52 
54 
     | 
    
         
             
            - spec/commands_spec.rb
         
     | 
| 
      
 55 
     | 
    
         
            +
            - spec/complete_spec.rb
         
     | 
| 
       53 
56 
     | 
    
         
             
            - spec/settings_spec.rb
         
     | 
| 
       54 
57 
     | 
    
         
             
            - spec/state_spec.rb
         
     | 
| 
      
 58 
     | 
    
         
            +
            - spec/text_spec.rb
         
     | 
| 
       55 
59 
     | 
    
         
             
            homepage: https://github.com/jangler/droxi
         
     | 
| 
       56 
60 
     | 
    
         
             
            licenses:
         
     | 
| 
       57 
61 
     | 
    
         
             
            - MIT
         
     |