droxi 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,43 +1,43 @@
1
1
  # Module containing tab-completion logic and methods.
2
2
  module Complete
3
+ # Return an +Array+ of potential command name tab-completions for a +String+.
4
+ def self.command(string, names)
5
+ names.select { |n| n.start_with?(string) }.map { |n| n + ' ' }
6
+ end
3
7
 
4
8
  # Return the directory in which to search for potential local tab-completions
5
9
  # for a +String+. Defaults to working directory in case of bogus input.
6
10
  def self.local_search_path(string)
7
- begin
8
- File.expand_path(strip_filename(string))
9
- rescue
10
- Dir.pwd
11
- end
11
+ File.expand_path(strip_filename(string))
12
+ rescue ArgumentError
13
+ Dir.pwd
12
14
  end
13
15
 
14
16
  # Return an +Array+ of potential local tab-completions for a +String+.
15
17
  def self.local(string)
16
18
  dir = local_search_path(string)
17
- name = string.end_with?('/') ? '' : File.basename(string)
19
+ basename = basename(string)
18
20
 
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]
21
+ matches = Dir.entries(dir).select { |entry| match?(basename, entry) }
22
+ matches.map do |entry|
23
+ final_match(string, entry, File.directory?(dir + '/' + entry))
24
24
  end
25
25
  end
26
26
 
27
27
  # Return an +Array+ of potential local tab-completions for a +String+,
28
28
  # including only directories.
29
29
  def self.local_dir(string)
30
- local(string).select { |result| result.end_with?('/') }
30
+ local(string).select { |match| match.end_with?('/') }
31
31
  end
32
32
 
33
33
  # Return the directory in which to search for potential remote
34
34
  # tab-completions for a +String+.
35
35
  def self.remote_search_path(string, state)
36
36
  path = case
37
- when string.empty? then state.pwd + '/'
38
- when string.start_with?('/') then string
39
- else state.pwd + '/' + string
40
- end
37
+ when string.empty? then state.pwd + '/'
38
+ when string.start_with?('/') then string
39
+ else state.pwd + '/' + string
40
+ end
41
41
 
42
42
  strip_filename(collapse(path))
43
43
  end
@@ -45,15 +45,12 @@ module Complete
45
45
  # Return an +Array+ of potential remote tab-completions for a +String+.
46
46
  def self.remote(string, state)
47
47
  dir = remote_search_path(string, state)
48
- name = string.end_with?('/') ? '' : File.basename(string)
48
+ basename = basename(string)
49
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]
50
+ entries = state.contents(dir).map { |entry| File.basename(entry) }
51
+ matches = entries.select { |entry| match?(basename, entry) }
52
+ matches.map do |entry|
53
+ final_match(string, entry, state.directory?(dir + '/' + entry))
57
54
  end
58
55
  end
59
56
 
@@ -65,22 +62,30 @@ module Complete
65
62
 
66
63
  private
67
64
 
65
+ def self.basename(string)
66
+ string.end_with?('/') ? '' : File.basename(string)
67
+ end
68
+
69
+ def self.match?(prefix, candidate)
70
+ candidate.start_with?(prefix) && !/^\.\.?$/.match(candidate)
71
+ end
72
+
73
+ def self.final_match(string, candidate, is_dir)
74
+ string + candidate.partition(basename(string))[2] + (is_dir ? '/' : ' ')
75
+ end
76
+
68
77
  # Return the name of the directory indicated by a path.
69
78
  def self.strip_filename(path)
70
- if path != '/'
71
- path.end_with?('/') ? path.sub(/\/$/, '') : File.dirname(path)
72
- else
73
- path
74
- end
79
+ return path if path == '/'
80
+ path.end_with?('/') ? path.sub(/\/$/, '') : File.dirname(path)
75
81
  end
76
82
 
77
83
  # Return a version of a path with .. and . resolved to appropriate
78
84
  # directories.
79
85
  def self.collapse(path)
80
86
  new_path = path.dup
81
- nil while new_path.sub!(/[^\/]+\/\.\.\//, '/')
87
+ nil while new_path.sub!(%r{[^/]+/\.\./}, '/')
82
88
  nil while new_path.sub!('./', '')
83
89
  new_path
84
90
  end
85
-
86
91
  end
@@ -1,85 +1,84 @@
1
1
  # Manages persistent (session-independent) application state.
2
- class Settings
2
+ module Settings
3
+ class << self
4
+ # The path of the application's rc file.
5
+ attr_accessor :config_file_path
6
+ end
3
7
 
4
- # The path of the application's rc file.
5
- CONFIG_FILE_PATH = File.expand_path('~/.config/droxi/droxirc')
8
+ self.config_file_path = File.expand_path('~/.config/droxi/droxirc')
6
9
 
7
10
  # Return the value of a setting, or +nil+ if the setting does not exist.
8
- def Settings.[](key)
9
- @@settings[key]
11
+ def self.[](key)
12
+ settings[key]
10
13
  end
11
14
 
12
15
  # Set the value of a setting.
13
- def Settings.[]=(key, value)
14
- if value != @@settings[key]
15
- @@dirty = true
16
- @@settings[key] = value
17
- end
16
+ def self.[]=(key, value)
17
+ return value if value == settings[key]
18
+ self.dirty = true
19
+ settings[key] = value
18
20
  end
19
21
 
20
22
  # Return +true+ if the setting exists, +false+ otherwise.
21
- def Settings.include?(key)
22
- @@settings.include?(key)
23
+ def self.include?(key)
24
+ settings.include?(key)
23
25
  end
24
26
 
25
27
  # Delete the setting and return its value.
26
- def Settings.delete(key)
27
- if @@settings.include?(key)
28
- @@dirty = true
29
- @@settings.delete(key)
30
- end
28
+ def self.delete(key)
29
+ return unless settings.include?(key)
30
+ self.dirty = true
31
+ settings.delete(key)
31
32
  end
32
33
 
33
34
  # Write settings to disk.
34
- def Settings.save
35
- if @@dirty
36
- @@dirty = false
37
- require 'fileutils'
38
- FileUtils.mkdir_p(File.dirname(CONFIG_FILE_PATH))
39
- File.open(CONFIG_FILE_PATH, 'w') do |file|
40
- @@settings.each_pair { |k, v| file.write("#{k}=#{v}\n") }
41
- file.chmod(0600)
42
- end
35
+ def self.save
36
+ return unless dirty
37
+ self.dirty = false
38
+ require 'fileutils'
39
+ FileUtils.mkdir_p(File.dirname(config_file_path))
40
+ File.open(config_file_path, 'w') do |file|
41
+ settings.each_pair { |k, v| file.write("#{k}=#{v}\n") }
42
+ file.chmod(0600)
43
43
  end
44
44
  nil
45
45
  end
46
46
 
47
+ # Read and parse the rc file.
48
+ def self.read
49
+ self.dirty = false
50
+ return {} unless File.exist?(config_file_path)
51
+ File.open(config_file_path) do |file|
52
+ file.each_line.reduce({}) { |a, e| a.merge(parse(e.strip)) }
53
+ end
54
+ end
55
+
47
56
  private
48
57
 
58
+ class << self
59
+ # +true+ if the settings have been modified since last write, +false+
60
+ # otherwise.
61
+ attr_accessor :dirty
62
+
63
+ # A +Hash+ of setting keys to values.
64
+ attr_accessor :settings
65
+ end
66
+
49
67
  # Print a warning for an invalid setting and return an empty +Hash+ (the
50
68
  # result of an invalid setting).
51
- def Settings.warn_invalid(line)
69
+ def self.warn_invalid(line)
52
70
  warn "invalid setting: #{line}"
53
71
  {}
54
72
  end
55
73
 
56
74
  # Parse a line of the rc file and return a +Hash+ containing the resulting
57
75
  # setting data.
58
- def Settings.parse(line)
59
- if /^(.+?)=(.+)$/ =~ line
60
- key, value = $1.to_sym, $2
61
- if [:access_token, :oldpwd].include?(key)
62
- {key => value}
63
- else
64
- warn_invalid(line)
65
- end
66
- else
67
- warn_invalid(line)
68
- end
69
- end
70
-
71
- # Read and parse the rc file.
72
- def Settings.read
73
- if File.exists?(CONFIG_FILE_PATH)
74
- File.open(CONFIG_FILE_PATH) do |file|
75
- file.each_line.reduce({}) { |a, e| a.merge(parse(e.strip)) }
76
- end
77
- else
78
- {}
79
- end
76
+ def self.parse(line)
77
+ return warn_invalid(line) unless /^(.+?)=(.+)$/ =~ line
78
+ key, value = Regexp.last_match[1].to_sym, Regexp.last_match[2]
79
+ return warn_invalid(line) unless [:access_token, :oldpwd].include?(key)
80
+ { key => value }
80
81
  end
81
82
 
82
- @@settings = read
83
- @@dirty = false
84
-
83
+ self.settings = read
85
84
  end
data/lib/droxi/state.rb CHANGED
@@ -4,9 +4,53 @@ require_relative 'settings'
4
4
  class GlobError < ArgumentError
5
5
  end
6
6
 
7
+ # Special +Hash+ of remote file paths to cached file metadata.
8
+ class Cache < Hash
9
+ # Add a metadata +Hash+ and its contents to the +Cache+ and return the
10
+ # +Cache+.
11
+ def add(metadata)
12
+ store(metadata['path'], metadata)
13
+ dirname = File.dirname(metadata['path'])
14
+ if dirname != metadata['path']
15
+ contents = fetch(dirname, {}).fetch('contents', nil)
16
+ contents << metadata if contents && !contents.include?(metadata)
17
+ end
18
+ return self unless metadata.include?('contents')
19
+ metadata['contents'].each { |content| add(content) }
20
+ self
21
+ end
22
+
23
+ # Remove a path from the +Cache+ and return the +Cache+.
24
+ def remove(path)
25
+ recursive_remove(path)
26
+
27
+ dir = File.dirname(path)
28
+ return self unless fetch(dir, {}).include?('contents')
29
+ fetch(dir)['contents'].delete_if { |item| item['path'] == path }
30
+
31
+ self
32
+ end
33
+
34
+ # Return +true+ if the path's information is cached, +false+ otherwise.
35
+ def full_info?(path, require_contents = true)
36
+ info = fetch(path, nil)
37
+ info && (!require_contents || !info['is_dir'] || info.include?('contents'))
38
+ end
39
+
40
+ private
41
+
42
+ # Recursively remove a path and its sub-files and directories.
43
+ def recursive_remove(path)
44
+ if fetch(path, {}).include?('contents')
45
+ fetch(path)['contents'].each { |item| recursive_remove(item['path']) }
46
+ end
47
+
48
+ delete(path)
49
+ end
50
+ end
51
+
7
52
  # Encapsulates the session state of the client.
8
53
  class State
9
-
10
54
  # +Hash+ of remote file paths to cached file metadata.
11
55
  attr_reader :cache
12
56
 
@@ -18,14 +62,14 @@ class State
18
62
 
19
63
  # The previous local working directory path.
20
64
  attr_accessor :local_oldpwd
21
-
65
+
22
66
  # +true+ if the client has requested to quit, +false+ otherwise.
23
67
  attr_accessor :exit_requested
24
68
 
25
69
  # Return a new application state that uses the given client. Starts at the
26
70
  # Dropbox root and with an empty cache.
27
71
  def initialize(client)
28
- @cache = {}
72
+ @cache = Cache.new
29
73
  @client = client
30
74
  @exit_requested = false
31
75
  @pwd = '/'
@@ -33,50 +77,15 @@ class State
33
77
  @local_oldpwd = Dir.pwd
34
78
  end
35
79
 
36
- # Adds a metadata +Hash+ and its contents to the metadata cache.
37
- def cache_add(metadata)
38
- @cache[metadata['path']] = metadata
39
- if metadata.include?('contents')
40
- metadata['contents'].each { |content| cache_add(content) }
41
- end
42
- end
43
-
44
- # Removes a path from the metadata cache.
45
- def cache_remove(path)
46
- if directory?(path) && @cache[path].include?('contents')
47
- @cache[path]['contents'].each { |item| cache_remove(item['path']) }
48
- end
49
-
50
- @cache.delete(path)
51
-
52
- dir = File.dirname(path)
53
- if @cache.include?(dir) && @cache[dir].include?('contents')
54
- @cache[dir]['contents'].delete_if { |item| item['path'] == path }
55
- end
56
- end
57
-
58
80
  # Return a +Hash+ of the Dropbox metadata for a file, or +nil+ if the file
59
81
  # does not exist.
60
- def metadata(path, require_contents=true)
82
+ def metadata(path, require_contents = true)
61
83
  tokens = path.split('/').drop(1)
62
84
 
63
- for i in 0..tokens.length
85
+ (0..tokens.length).each do |i|
64
86
  partial_path = '/' + tokens.take(i).join('/')
65
- unless have_all_info_for(partial_path, require_contents)
66
- begin
67
- data = @client.metadata(partial_path)
68
- if !data['is_deleted']
69
- @cache[partial_path] = data
70
- if data.include?('contents')
71
- data['contents'].each do |datum|
72
- @cache[datum['path']] = datum
73
- end
74
- end
75
- end
76
- rescue DropboxError
77
- return nil
78
- end
79
- end
87
+ next if @cache.full_info?(partial_path, require_contents)
88
+ return nil unless fetch_metadata(partial_path)
80
89
  end
81
90
 
82
91
  @cache[path]
@@ -110,17 +119,16 @@ class State
110
119
  def resolve_path(arg)
111
120
  path = arg.start_with?('/') ? arg.dup : "#{@pwd}/#{arg}"
112
121
  path.gsub!('//', '/')
113
- nil while path.sub!(/\/([^\/]+?)\/\.\./, '')
122
+ nil while path.sub!(%r{/([^/]+?)/\.\.}, '')
114
123
  nil while path.sub!('./', '')
115
124
  path.sub!(/\/\.$/, '')
116
125
  path.chomp!('/')
117
- path = '/' if path.empty?
118
- path
126
+ path.empty? ? '/' : path
119
127
  end
120
128
 
121
129
  # Expand an +Array+ of file globs into an an +Array+ of Dropbox file paths
122
130
  # and return the result.
123
- def expand_patterns(patterns, preserve_root=false)
131
+ def expand_patterns(patterns, preserve_root = false)
124
132
  patterns.map do |pattern|
125
133
  path = resolve_path(pattern)
126
134
  if directory?(path)
@@ -135,40 +143,35 @@ class State
135
143
  # (error) output if a block is given.
136
144
  def forget_contents(partial_path)
137
145
  path = resolve_path(partial_path)
138
- if @cache.include?(path) && @cache[path].include?('contents')
146
+ if @cache.fetch(path, {}).include?('contents')
147
+ @cache[path]['contents'].dup.each { |m| @cache.remove(m['path']) }
139
148
  @cache[path].delete('contents')
140
- @cache.keys.each do |key|
141
- @cache.delete(key) if key.start_with?(path) && key != path
142
- end
143
149
  elsif block_given?
144
- yield "forget: #{partial_path}: Nothing to forget"
150
+ yield "forget: #{partial_path}: nothing to forget"
145
151
  end
146
152
  end
147
153
 
148
154
  private
149
155
 
156
+ # Cache metadata for the remote file for a given path. Return +true+ if
157
+ # successful, +false+ otherwise.
158
+ def fetch_metadata(path)
159
+ data = @client.metadata(path)
160
+ return true if data['is_deleted']
161
+ @cache.add(data)
162
+ true
163
+ rescue DropboxError
164
+ false
165
+ end
166
+
150
167
  # Return an +Array+ of file paths matching a glob pattern, or a GlobError if
151
168
  # no files were matched.
152
169
  def get_matches(pattern, path, preserve_root)
153
170
  dir = File.dirname(path)
154
171
  matches = contents(dir).select { |entry| File.fnmatch(path, entry) }
155
- if matches.empty?
156
- GlobError.new(pattern)
157
- elsif preserve_root
158
- prefix = pattern.rpartition('/')[0, 2].join
159
- matches.map { |match| prefix + match.rpartition('/')[2] }
160
- else
161
- matches
162
- end
172
+ return GlobError.new(pattern) if matches.empty?
173
+ return matches unless preserve_root
174
+ prefix = pattern.rpartition('/')[0, 2].join
175
+ matches.map { |match| prefix + match.rpartition('/')[2] }
163
176
  end
164
-
165
- # Return +true+ if the path's information is cached, +false+ otherwise.
166
- def have_all_info_for(path, require_contents=true)
167
- @cache.include?(path) && (
168
- !require_contents ||
169
- !@cache[path]['is_dir'] ||
170
- @cache[path].include?('contents')
171
- )
172
- end
173
-
174
177
  end