sup 0.19.0 → 0.23

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.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +12 -6
  5. data/CONTRIBUTORS +28 -14
  6. data/Gemfile +5 -0
  7. data/History.txt +92 -0
  8. data/README.md +26 -5
  9. data/Rakefile +41 -1
  10. data/ReleaseNotes +17 -0
  11. data/bin/sup +12 -23
  12. data/bin/sup-add +15 -16
  13. data/bin/sup-config +30 -45
  14. data/bin/sup-dump +2 -3
  15. data/bin/sup-import-dump +5 -6
  16. data/bin/sup-sync +3 -4
  17. data/bin/sup-sync-back-maildir +3 -4
  18. data/bin/sup-tweak-labels +6 -7
  19. data/contrib/colorpicker.rb +0 -2
  20. data/contrib/completion/_sup.bash +102 -0
  21. data/devel/profile.rb +0 -1
  22. data/ext/mkrf_conf_xapian.rb +47 -0
  23. data/lib/sup.rb +10 -8
  24. data/lib/sup/buffer.rb +12 -0
  25. data/lib/sup/colormap.rb +5 -2
  26. data/lib/sup/contact.rb +4 -2
  27. data/lib/sup/crypto.rb +58 -16
  28. data/lib/sup/draft.rb +8 -8
  29. data/lib/sup/hook.rb +9 -9
  30. data/lib/sup/index.rb +20 -7
  31. data/lib/sup/label.rb +1 -1
  32. data/lib/sup/logger.rb +1 -1
  33. data/lib/sup/maildir.rb +16 -5
  34. data/lib/sup/mbox.rb +13 -5
  35. data/lib/sup/message.rb +36 -12
  36. data/lib/sup/message_chunks.rb +13 -4
  37. data/lib/sup/mode.rb +34 -28
  38. data/lib/sup/modes/contact_list_mode.rb +1 -0
  39. data/lib/sup/modes/edit_message_mode.rb +3 -2
  40. data/lib/sup/modes/forward_mode.rb +22 -3
  41. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  42. data/lib/sup/modes/reply_mode.rb +3 -1
  43. data/lib/sup/modes/text_mode.rb +6 -1
  44. data/lib/sup/modes/thread_index_mode.rb +12 -2
  45. data/lib/sup/modes/thread_view_mode.rb +111 -14
  46. data/lib/sup/person.rb +68 -61
  47. data/lib/sup/search.rb +1 -1
  48. data/lib/sup/sent.rb +1 -1
  49. data/lib/sup/source.rb +1 -1
  50. data/lib/sup/util.rb +15 -94
  51. data/lib/sup/util/axe.rb +17 -0
  52. data/lib/sup/util/locale_fiddler.rb +24 -0
  53. data/lib/sup/util/ncurses.rb +3 -3
  54. data/lib/sup/version.rb +10 -1
  55. data/sup.gemspec +29 -11
  56. data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
  57. data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
  58. data/test/fixtures/blank-header-fields.eml +71 -0
  59. data/test/fixtures/contacts.txt +1 -0
  60. data/test/fixtures/mailing-list-header.eml +80 -0
  61. data/test/fixtures/malicious-attachment-names.eml +55 -0
  62. data/test/fixtures/missing-from-to.eml +18 -0
  63. data/test/{messages → fixtures}/missing-line.eml +0 -0
  64. data/test/fixtures/multi-part-2.eml +72 -0
  65. data/test/fixtures/multi-part.eml +61 -0
  66. data/test/fixtures/no-body.eml +18 -0
  67. data/test/fixtures/simple-message.eml +29 -0
  68. data/test/fixtures/text-attachments-with-charset.eml +46 -0
  69. data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
  70. data/test/gnupg_test_home/gpg.conf +3 -1
  71. data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
  72. data/test/gnupg_test_home/pubring.gpg +0 -0
  73. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  74. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  75. data/test/gnupg_test_home/regen_keys.sh +89 -0
  76. data/test/gnupg_test_home/secring.gpg +0 -0
  77. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -17
  78. data/test/integration/test_maildir.rb +75 -0
  79. data/test/integration/test_mbox.rb +69 -0
  80. data/test/test_crypto.rb +14 -2
  81. data/test/test_header_parsing.rb +1 -1
  82. data/test/test_helper.rb +6 -3
  83. data/test/test_message.rb +115 -341
  84. data/test/test_messages_dir.rb +4 -28
  85. data/test/test_yaml_regressions.rb +1 -1
  86. data/test/unit/test_contact.rb +33 -0
  87. data/test/unit/test_locale_fiddler.rb +15 -0
  88. data/test/unit/test_person.rb +37 -0
  89. data/test/unit/util/test_query.rb +10 -4
  90. data/test/unit/util/test_string.rb +6 -0
  91. metadata +137 -53
  92. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  93. data/test/gnupg_test_home/trustdb.gpg +0 -0
@@ -105,7 +105,7 @@ EOS
105
105
 
106
106
  def save
107
107
  debug "saving index and sources..."
108
- FileUtils.mkdir_p @dir unless File.exists? @dir
108
+ FileUtils.mkdir_p @dir unless File.exist? @dir
109
109
  SourceManager.save_sources
110
110
  save_index
111
111
  end
@@ -116,7 +116,7 @@ EOS
116
116
 
117
117
  def load_index failsafe=false
118
118
  path = File.join(@dir, 'xapian')
119
- if File.exists? path
119
+ if File.exist? path
120
120
  @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
121
121
  db_version = @xapian.get_metadata 'version'
122
122
  db_version = '0' if db_version.empty?
@@ -516,19 +516,32 @@ EOS
516
516
  qp.stemmer = Xapian::Stem.new($config[:stem_language])
517
517
  qp.stemming_strategy = Xapian::QueryParser::STEM_SOME
518
518
  qp.default_op = Xapian::Query::OP_AND
519
- qp.add_valuerangeprocessor(Xapian::NumberValueRangeProcessor.new(DATE_VALUENO, 'date:', true))
520
- NORMAL_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_prefix k, v } }
521
- BOOLEAN_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_boolean_prefix k, v, info[:exclusive] } }
519
+ valuerangeprocessor = Xapian::NumberValueRangeProcessor.new(DATE_VALUENO,
520
+ 'date:', true)
521
+ qp.add_valuerangeprocessor(valuerangeprocessor)
522
+ NORMAL_PREFIX.each { |k,info| info[:prefix].each {
523
+ |v| qp.add_prefix k, v }
524
+ }
525
+ BOOLEAN_PREFIX.each { |k,info| info[:prefix].each {
526
+ |v| qp.add_boolean_prefix k, v, info[:exclusive] }
527
+ }
522
528
 
523
529
  begin
524
- xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE|Xapian::QueryParser::FLAG_BOOLEAN|Xapian::QueryParser::FLAG_LOVEHATE|Xapian::QueryParser::FLAG_WILDCARD)
530
+ xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE |
531
+ Xapian::QueryParser::FLAG_BOOLEAN |
532
+ Xapian::QueryParser::FLAG_LOVEHATE |
533
+ Xapian::QueryParser::FLAG_WILDCARD)
525
534
  rescue RuntimeError => e
526
535
  raise ParseError, "xapian query parser error: #{e}"
527
536
  end
528
537
 
529
538
  debug "parsed xapian query: #{Util::Query.describe(xapian_query, subs)}"
530
539
 
531
- raise ParseError if xapian_query.nil? or xapian_query.empty?
540
+ if xapian_query.nil? or xapian_query.empty?
541
+ raise ParseError, "couldn't parse \"#{s}\" as xapian query " \
542
+ "(special characters aren't indexed)"
543
+ end
544
+
532
545
  query[:qobj] = xapian_query
533
546
  query[:text] = s
534
547
  query
@@ -15,7 +15,7 @@ class LabelManager
15
15
  def initialize fn
16
16
  @fn = fn
17
17
  labels =
18
- if File.exists? fn
18
+ if File.exist? fn
19
19
  IO.readlines(fn).map { |x| x.chomp.intern }
20
20
  else
21
21
  []
@@ -71,7 +71,7 @@ end
71
71
 
72
72
  ## include me to have top-level #debug, #info, etc. methods.
73
73
  module LogsStuff
74
- Logger::LEVELS.each { |l| define_method(l) { |s| Logger.instance.send(l, s) } }
74
+ Logger::LEVELS.each { |l| define_method(l) { |s, uplevel = 0| Logger.instance.send(l, s) } }
75
75
  end
76
76
 
77
77
  end
@@ -12,7 +12,15 @@ class Maildir < Source
12
12
  def initialize uri, usual=true, archived=false, sync_back=true, id=nil, labels=[]
13
13
  super uri, usual, archived, id
14
14
  @expanded_uri = Source.expand_filesystem_uri(uri)
15
- uri = URI(@expanded_uri)
15
+ parts = @expanded_uri.match /^([a-zA-Z0-9]*:(\/\/)?)(.*)/
16
+ if parts
17
+ prefix = parts[1]
18
+ @path = parts[3]
19
+ uri = URI(prefix + URI.encode(@path, URI_ENCODE_CHARS))
20
+ else
21
+ uri = URI(URI.encode @expanded_uri, URI_ENCODE_CHARS)
22
+ @path = uri.path
23
+ end
16
24
 
17
25
  raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
18
26
  raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
@@ -22,7 +30,7 @@ class Maildir < Source
22
30
  # sync by default if not specified
23
31
  @sync_back = true if @sync_back.nil?
24
32
 
25
- @dir = uri.path
33
+ @dir = URI.decode uri.path
26
34
  @labels = Set.new(labels || [])
27
35
  @mutex = Mutex.new
28
36
  @ctimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
@@ -60,7 +68,7 @@ class Maildir < Source
60
68
  File.safe_link tmp_path, new_path
61
69
  stored = true
62
70
  ensure
63
- File.unlink tmp_path if File.exists? tmp_path
71
+ File.unlink tmp_path if File.exist? tmp_path
64
72
  end
65
73
  end #rescue Errno...
66
74
  end #Dir.chdir
@@ -120,7 +128,10 @@ class Maildir < Source
120
128
  @ctimes[d] = ctime
121
129
 
122
130
  old_ids = benchmark(:maildir_read_index) { Index.instance.enum_for(:each_source_info, self.id, "#{d}/").to_a }
123
- new_ids = benchmark(:maildir_read_dir) { Dir.glob("#{subdir}/*").map { |x| File.join(d,File.basename(x)) }.sort }
131
+ new_ids = benchmark(:maildir_read_dir) {
132
+ Dir.open(subdir).select {
133
+ |f| !File.directory? f}.map {
134
+ |x| File.join(d,File.basename(x)) }.sort }
124
135
  added += new_ids - old_ids
125
136
  deleted += old_ids - new_ids
126
137
  debug "#{old_ids.size} in index, #{new_ids.size} in filesystem"
@@ -190,7 +201,7 @@ class Maildir < Source
190
201
  def trashed? id; maildir_data(id)[2].include? "T"; end
191
202
 
192
203
  def valid? id
193
- File.exists? File.join(@dir, id)
204
+ File.exist? File.join(@dir, id)
194
205
  end
195
206
 
196
207
  private
@@ -19,16 +19,24 @@ class MBox < Source
19
19
  case uri_or_fp
20
20
  when String
21
21
  @expanded_uri = Source.expand_filesystem_uri(uri_or_fp)
22
- uri = URI(@expanded_uri)
22
+ parts = @expanded_uri.match /^([a-zA-Z0-9]*:(\/\/)?)(.*)/
23
+ if parts
24
+ prefix = parts[1]
25
+ @path = parts[3]
26
+ uri = URI(prefix + URI.encode(@path, URI_ENCODE_CHARS))
27
+ else
28
+ uri = URI(URI.encode @expanded_uri, URI_ENCODE_CHARS)
29
+ @path = uri.path
30
+ end
31
+
23
32
  raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
24
33
  raise ArgumentError, "mbox URI ('#{uri}') cannot have a host: #{uri.host}" if uri.host
25
34
  raise ArgumentError, "mbox URI must have a path component" unless uri.path
26
35
  @f = nil
27
- @path = uri.path
28
36
  else
29
37
  @f = uri_or_fp
30
38
  @path = uri_or_fp.path
31
- @expanded_uri = "mbox://#{@path}"
39
+ @expanded_uri = "mbox://#{URI.encode @path, URI_ENCODE_CHARS}"
32
40
  end
33
41
 
34
42
  super uri_or_fp, usual, archived, id
@@ -107,7 +115,7 @@ class MBox < Source
107
115
  end
108
116
 
109
117
  def store_message date, from_email, &block
110
- need_blank = File.exists?(@path) && !File.zero?(@path)
118
+ need_blank = File.exist?(@path) && !File.zero?(@path)
111
119
  File.open(@path, "ab") do |f|
112
120
  f.puts if need_blank
113
121
  f.puts "From #{from_email} #{date.asctime}"
@@ -172,7 +180,7 @@ class MBox < Source
172
180
  time = $1
173
181
  begin
174
182
  ## hack -- make Time.parse fail when trying to substitute values from Time.now
175
- Time.parse time, 0
183
+ Time.parse time, Time.at(0)
176
184
  true
177
185
  rescue NoMethodError, ArgumentError
178
186
  warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
@@ -136,6 +136,11 @@ class Message
136
136
  header["list-post"] # just try the whole fucking thing
137
137
  end
138
138
  address && Person.from_address(address)
139
+ elsif header["mailing-list"]
140
+ address = if header["mailing-list"] =~ /list (.*?);/
141
+ $1
142
+ end
143
+ address && Person.from_address(address)
139
144
  elsif header["x-mailing-list"]
140
145
  Person.from_address header["x-mailing-list"]
141
146
  end
@@ -264,13 +269,27 @@ class Message
264
269
  parse_header rmsg.header
265
270
  message_to_chunks rmsg
266
271
  rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
267
- warn "problem reading message #{id}"
272
+ warn_with_location "problem reading message #{id}"
268
273
  debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
269
274
 
270
275
  [Chunk::Text.new(error_message.split("\n"))]
276
+
277
+ rescue Exception => e
278
+
279
+ warn_with_location "problem reading message #{id}"
280
+ debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
281
+
282
+ raise e
283
+
271
284
  end
272
285
  end
273
286
 
287
+ def reload_from_source!
288
+ @chunks = nil
289
+ load_from_source!
290
+ end
291
+
292
+
274
293
  def error_message
275
294
  <<EOS
276
295
  #@snippet...
@@ -327,17 +346,17 @@ EOS
327
346
  to.map { |p| p.indexable_content },
328
347
  cc.map { |p| p.indexable_content },
329
348
  bcc.map { |p| p.indexable_content },
330
- indexable_chunks.map { |c| c.lines },
349
+ indexable_chunks.map { |c| c.lines.map { |l| l.fix_encoding! } },
331
350
  indexable_subject,
332
351
  ].flatten.compact.join " "
333
352
  end
334
353
 
335
354
  def indexable_body
336
- indexable_chunks.map { |c| c.lines }.flatten.compact.join " "
355
+ indexable_chunks.map { |c| c.lines }.flatten.compact.map { |l| l.fix_encoding! }.join " "
337
356
  end
338
357
 
339
358
  def indexable_chunks
340
- chunks.select { |c| c.is_a? Chunk::Text } || []
359
+ chunks.select { |c| c.indexable? } || []
341
360
  end
342
361
 
343
362
  def indexable_subject
@@ -390,19 +409,19 @@ private
390
409
 
391
410
  def multipart_signed_to_chunks m
392
411
  if m.body.size != 2
393
- warn "multipart/signed with #{m.body.size} parts (expecting 2)"
412
+ warn_with_location "multipart/signed with #{m.body.size} parts (expecting 2)"
394
413
  return
395
414
  end
396
415
 
397
416
  payload, signature = m.body
398
417
  if signature.multipart?
399
- warn "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
418
+ warn_with_location "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
400
419
  return
401
420
  end
402
421
 
403
422
  ## this probably will never happen
404
423
  if payload.header.content_type && payload.header.content_type.downcase == "application/pgp-signature"
405
- warn "multipart/signed with payload content type #{payload.header.content_type}"
424
+ warn_with_location "multipart/signed with payload content type #{payload.header.content_type}"
406
425
  return
407
426
  end
408
427
 
@@ -417,23 +436,23 @@ private
417
436
 
418
437
  def multipart_encrypted_to_chunks m
419
438
  if m.body.size != 2
420
- warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
439
+ warn_with_location "multipart/encrypted with #{m.body.size} parts (expecting 2)"
421
440
  return
422
441
  end
423
442
 
424
443
  control, payload = m.body
425
444
  if control.multipart?
426
- warn "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
445
+ warn_with_location "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
427
446
  return
428
447
  end
429
448
 
430
449
  if payload.header.content_type && payload.header.content_type.downcase != "application/octet-stream"
431
- warn "multipart/encrypted with payload content type #{payload.header.content_type}"
450
+ warn_with_location "multipart/encrypted with payload content type #{payload.header.content_type}"
432
451
  return
433
452
  end
434
453
 
435
454
  if control.header.content_type && control.header.content_type.downcase != "application/pgp-encrypted"
436
- warn "multipart/encrypted with control content type #{signature.header.content_type}"
455
+ warn_with_location "multipart/encrypted with control content type #{signature.header.content_type}"
437
456
  return
438
457
  end
439
458
 
@@ -677,7 +696,7 @@ private
677
696
  newstate = :quote
678
697
  elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE && !lines[(i+1)..-1].index { |l| l =~ /^-- $/ }
679
698
  newstate = :sig
680
- elsif line =~ BLOCK_QUOTE_PATTERN
699
+ elsif line =~ BLOCK_QUOTE_PATTERN && nextline !~ QUOTE_PATTERN
681
700
  newstate = :block_quote
682
701
  end
683
702
 
@@ -737,6 +756,11 @@ private
737
756
  end
738
757
  chunks
739
758
  end
759
+
760
+ def warn_with_location msg
761
+ warn msg
762
+ warn "Message is in #{location.source.uri} at #{location.info}"
763
+ end
740
764
  end
741
765
 
742
766
  class Location
@@ -128,7 +128,7 @@ EOS
128
128
 
129
129
  text = case @content_type
130
130
  when /^text\/plain\b/
131
- @raw_content
131
+ @raw_content.force_encoding(encoded_content.charset || 'US-ASCII')
132
132
  else
133
133
  HookManager.run "mime-decode", :content_type => @content_type,
134
134
  :filename => lambda { write_to_disk },
@@ -138,7 +138,7 @@ EOS
138
138
 
139
139
  @lines = nil
140
140
  if text
141
- text = text.transcode(encoded_content.charset || $encoding, text.encoding)
141
+ text = text.encode($encoding, :invalid => :replace, :undef => :replace)
142
142
  begin
143
143
  @lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
144
144
  rescue Encoding::CompatibilityError
@@ -159,11 +159,14 @@ EOS
159
159
  "Attachment: #{filename} (#{content_type}; #{@raw_content.size.to_human_size})"
160
160
  end
161
161
  end
162
+ def safe_filename; Shellwords.escape(@filename).gsub("/", "_") end
163
+ def filesafe_filename; @filename.gsub("/", "_") end
162
164
 
163
165
  ## an attachment is exapndable if we've managed to decode it into
164
166
  ## something we can display inline. otherwise, it's viewable.
165
167
  def inlineable?; false end
166
168
  def expandable?; !viewable? end
169
+ def indexable?; expandable? end
167
170
  def initial_state; :open end
168
171
  def viewable?; @lines.nil? end
169
172
  def view_default! path
@@ -229,6 +232,7 @@ EOS
229
232
  def inlineable?; true end
230
233
  def quotable?; true end
231
234
  def expandable?; false end
235
+ def indexable?; true end
232
236
  def viewable?; false end
233
237
  def color; :text_color end
234
238
  end
@@ -242,6 +246,7 @@ EOS
242
246
  def inlineable?; @lines.length == 1 end
243
247
  def quotable?; true end
244
248
  def expandable?; !inlineable? end
249
+ def indexable?; expandable? end
245
250
  def viewable?; false end
246
251
 
247
252
  def patina_color; :quote_patina_color end
@@ -258,6 +263,7 @@ EOS
258
263
  def inlineable?; @lines.length == 1 end
259
264
  def quotable?; false end
260
265
  def expandable?; !inlineable? end
266
+ def indexable?; expandable? end
261
267
  def viewable?; false end
262
268
 
263
269
  def patina_color; :sig_patina_color end
@@ -291,6 +297,7 @@ EOS
291
297
  def inlineable?; false end
292
298
  def quotable?; false end
293
299
  def expandable?; true end
300
+ def indexable?; true end
294
301
  def initial_state; :closed end
295
302
  def viewable?; false end
296
303
 
@@ -301,12 +308,13 @@ EOS
301
308
  end
302
309
 
303
310
  class CryptoNotice
304
- attr_reader :lines, :status, :patina_text
311
+ attr_reader :lines, :status, :patina_text, :unknown_fingerprint
305
312
 
306
- def initialize status, description, lines=[]
313
+ def initialize status, description, lines=[], unknown_fingerprint=nil
307
314
  @status = status
308
315
  @patina_text = description
309
316
  @lines = lines
317
+ @unknown_fingerprint = unknown_fingerprint
310
318
  end
311
319
 
312
320
  def patina_color
@@ -322,6 +330,7 @@ EOS
322
330
  def inlineable?; false end
323
331
  def quotable?; false end
324
332
  def expandable?; !@lines.empty? end
333
+ def indexable?; false end
325
334
  def viewable?; false end
326
335
  end
327
336
  end
@@ -46,7 +46,7 @@ class Mode
46
46
  end
47
47
 
48
48
  def resolve_input c
49
- ancestors.each do |klass| # try all keymaps in order of ancestry
49
+ self.class.ancestors.each do |klass| # try all keymaps in order of ancestry
50
50
  next unless @@keymaps.member?(klass)
51
51
  action = BufferManager.resolve_input_with_keymap c, @@keymaps[klass]
52
52
  return action if action
@@ -62,7 +62,7 @@ class Mode
62
62
 
63
63
  def help_text
64
64
  used_keys = {}
65
- ancestors.map do |klass|
65
+ self.class.ancestors.map do |klass|
66
66
  km = @@keymaps[klass] or next
67
67
  title = "Keybindings from #{Mode.make_name klass.name}"
68
68
  s = <<EOS
@@ -83,7 +83,8 @@ EOS
83
83
  ### helper functions
84
84
 
85
85
  def save_to_file fn, talk=true
86
- if File.exists? fn
86
+ FileUtils.mkdir_p File.dirname(fn)
87
+ if File.exist? fn
87
88
  unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
88
89
  info "Not overwriting #{fn}"
89
90
  return
@@ -102,37 +103,42 @@ EOS
102
103
  end
103
104
 
104
105
  def pipe_to_process command
105
- Open3.popen3(command) do |input, output, error|
106
- err, data, * = IO.select [error], [input], nil
107
-
108
- unless err.empty?
109
- message = err.first.read
110
- if message =~ /^\s*$/
111
- warn "error running #{command} (but no error message)"
112
- BufferManager.flash "Error running #{command}!"
113
- else
114
- warn "error running #{command}: #{message}"
115
- BufferManager.flash "Error: #{message}"
106
+ begin
107
+ Open3.popen3(command) do |input, output, error|
108
+ err, data, * = IO.select [error], [input], nil
109
+
110
+ unless err.empty?
111
+ message = err.first.read
112
+ if message =~ /^\s*$/
113
+ warn "error running #{command} (but no error message)"
114
+ BufferManager.flash "Error running #{command}!"
115
+ else
116
+ warn "error running #{command}: #{message}"
117
+ BufferManager.flash "Error: #{message}"
118
+ end
119
+ return nil, false
116
120
  end
117
- return
118
- end
119
121
 
120
- data = data.first
121
- data.sync = false # buffer input
122
+ data = data.first
123
+ data.sync = false # buffer input
122
124
 
123
- yield data
124
- data.close # output will block unless input is closed
125
+ yield data
126
+ data.close # output will block unless input is closed
125
127
 
126
- ## BUG?: shows errors or output but not both....
127
- data, * = IO.select [output, error], nil, nil
128
- data = data.first
128
+ ## BUG?: shows errors or output but not both....
129
+ data, * = IO.select [output, error], nil, nil
130
+ data = data.first
129
131
 
130
- if data.eof
131
- BufferManager.flash "'#{command}' done!"
132
- nil
133
- else
134
- data.read
132
+ if data.eof
133
+ BufferManager.flash "'#{command}' done!"
134
+ return nil, true
135
+ else
136
+ return data.read, true
137
+ end
135
138
  end
139
+ rescue Errno::ENOENT
140
+ # If the command is invalid
141
+ return nil, false
136
142
  end
137
143
  end
138
144
  end