droxi 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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