sup 0.8.1 → 0.9

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (67) hide show
  1. data/CONTRIBUTORS +13 -6
  2. data/History.txt +19 -0
  3. data/ReleaseNotes +35 -0
  4. data/bin/sup +82 -77
  5. data/bin/sup-add +7 -7
  6. data/bin/sup-config +104 -85
  7. data/bin/sup-dump +4 -5
  8. data/bin/sup-recover-sources +9 -10
  9. data/bin/sup-sync +121 -100
  10. data/bin/sup-sync-back +18 -15
  11. data/bin/sup-tweak-labels +24 -21
  12. data/lib/sup.rb +53 -33
  13. data/lib/sup/account.rb +0 -2
  14. data/lib/sup/buffer.rb +47 -22
  15. data/lib/sup/colormap.rb +6 -6
  16. data/lib/sup/contact.rb +0 -2
  17. data/lib/sup/crypto.rb +34 -23
  18. data/lib/sup/draft.rb +6 -14
  19. data/lib/sup/ferret_index.rb +471 -0
  20. data/lib/sup/hook.rb +30 -43
  21. data/lib/sup/hook.rb.BACKUP.8625.rb +158 -0
  22. data/lib/sup/hook.rb.BACKUP.8681.rb +158 -0
  23. data/lib/sup/hook.rb.BASE.8625.rb +155 -0
  24. data/lib/sup/hook.rb.BASE.8681.rb +155 -0
  25. data/lib/sup/hook.rb.LOCAL.8625.rb +142 -0
  26. data/lib/sup/hook.rb.LOCAL.8681.rb +142 -0
  27. data/lib/sup/hook.rb.REMOTE.8625.rb +145 -0
  28. data/lib/sup/hook.rb.REMOTE.8681.rb +145 -0
  29. data/lib/sup/imap.rb +18 -8
  30. data/lib/sup/index.rb +70 -528
  31. data/lib/sup/interactive-lock.rb +74 -0
  32. data/lib/sup/keymap.rb +26 -26
  33. data/lib/sup/label.rb +2 -4
  34. data/lib/sup/logger.rb +54 -35
  35. data/lib/sup/maildir.rb +41 -6
  36. data/lib/sup/mbox.rb +1 -1
  37. data/lib/sup/mbox/loader.rb +18 -6
  38. data/lib/sup/mbox/ssh-file.rb +1 -7
  39. data/lib/sup/message-chunks.rb +36 -23
  40. data/lib/sup/message.rb +126 -46
  41. data/lib/sup/mode.rb +3 -2
  42. data/lib/sup/modes/console-mode.rb +108 -0
  43. data/lib/sup/modes/edit-message-mode.rb +15 -5
  44. data/lib/sup/modes/inbox-mode.rb +2 -4
  45. data/lib/sup/modes/label-list-mode.rb +1 -1
  46. data/lib/sup/modes/line-cursor-mode.rb +18 -18
  47. data/lib/sup/modes/log-mode.rb +29 -16
  48. data/lib/sup/modes/poll-mode.rb +7 -9
  49. data/lib/sup/modes/reply-mode.rb +5 -3
  50. data/lib/sup/modes/scroll-mode.rb +2 -2
  51. data/lib/sup/modes/search-results-mode.rb +9 -11
  52. data/lib/sup/modes/text-mode.rb +2 -2
  53. data/lib/sup/modes/thread-index-mode.rb +26 -16
  54. data/lib/sup/modes/thread-view-mode.rb +84 -39
  55. data/lib/sup/person.rb +6 -8
  56. data/lib/sup/poll.rb +46 -47
  57. data/lib/sup/rfc2047.rb +1 -5
  58. data/lib/sup/sent.rb +27 -20
  59. data/lib/sup/source.rb +90 -13
  60. data/lib/sup/textfield.rb +4 -4
  61. data/lib/sup/thread.rb +15 -13
  62. data/lib/sup/undo.rb +0 -1
  63. data/lib/sup/update.rb +0 -1
  64. data/lib/sup/util.rb +51 -43
  65. data/lib/sup/xapian_index.rb +566 -0
  66. metadata +57 -46
  67. data/lib/sup/suicide.rb +0 -36
@@ -52,11 +52,7 @@ module Rfc2047
52
52
  # WORD.
53
53
  end
54
54
 
55
- begin
56
- Iconv.easy_decode(target, charset, text)
57
- rescue Iconv::InvalidCharacter
58
- text
59
- end
55
+ Iconv.easy_decode(target, charset, text)
60
56
  end
61
57
  end
62
58
  end
@@ -3,28 +3,34 @@ module Redwood
3
3
  class SentManager
4
4
  include Singleton
5
5
 
6
- attr_accessor :source
7
- def initialize fn
8
- @fn = fn
6
+ attr_reader :source, :source_uri
7
+
8
+ def initialize source_uri
9
9
  @source = nil
10
- self.class.i_am_the_instance self
10
+ @source_uri = source_uri
11
11
  end
12
12
 
13
- def self.source_name; "sup://sent"; end
14
- def self.source_id; 9998; end
15
- def new_source; @source = Recoverable.new SentLoader.new; end
13
+ def source_id; @source.id; end
16
14
 
17
- def write_sent_message date, from_email
18
- need_blank = File.exists?(@fn) && !File.zero?(@fn)
19
- File.open(@fn, "a") do |f|
20
- f.puts if need_blank
21
- f.puts "From #{from_email} #{date}"
22
- yield f
23
- end
15
+ def source= s
16
+ raise FatalSourceError.new("Configured sent_source [#{s.uri}] can't store mail. Correct your configuration.") unless s.respond_to? :store_message
17
+ @souce_uri = s.uri
18
+ @source = s
19
+ end
24
20
 
25
- PollManager.add_messages_from(@source) do |m, o, e|
21
+ def default_source
22
+ @source = Recoverable.new SentLoader.new
23
+ @source_uri = @source.uri
24
+ @source
25
+ end
26
+
27
+ def write_sent_message date, from_email, &block
28
+ @source.store_message date, from_email, &block
29
+
30
+ PollManager.each_message_from(@source) do |m|
26
31
  m.remove_label :unread
27
- m
32
+ m.add_label :sent
33
+ PollManager.add_new_message m
28
34
  end
29
35
  end
30
36
  end
@@ -40,10 +46,11 @@ class SentLoader < MBox::Loader
40
46
 
41
47
  def file_path; @filename end
42
48
 
43
- def uri; SentManager.source_name; end
44
- def to_s; SentManager.source_name; end
45
- def id; SentManager.source_id; end
46
- def labels; [:sent, :inbox]; end
49
+ def to_s; 'sup://sent'; end
50
+ def uri; 'sup://sent' end
51
+
52
+ def id; 9998; end
53
+ def labels; [:inbox, :sent]; end
47
54
  end
48
55
 
49
56
  end
@@ -34,12 +34,12 @@ class Source
34
34
  ## To write a new source, subclass this class, and implement:
35
35
  ##
36
36
  ## - start_offset
37
- ## - end_offset (exclusive!)
37
+ ## - end_offset (exclusive!) (or, #done?)
38
38
  ## - load_header offset
39
39
  ## - load_message offset
40
40
  ## - raw_header offset
41
41
  ## - raw_message offset
42
- ## - check
42
+ ## - check (optional)
43
43
  ## - next (or each, if you prefer): should return a message and an
44
44
  ## array of labels.
45
45
  ##
@@ -78,6 +78,7 @@ class Source
78
78
  @dirty = false
79
79
  end
80
80
 
81
+ ## overwrite me if you have a disk incarnation (currently used only for sup-sync-back)
81
82
  def file_path; nil end
82
83
 
83
84
  def to_s; @uri.to_s; end
@@ -92,20 +93,23 @@ class Source
92
93
  ## to proactively notify the user of any source problems.
93
94
  def check; end
94
95
 
96
+ ## yields successive offsets and labels, starting at #cur_offset.
97
+ ##
98
+ ## when implementing a source, you can overwrite either #each or #next. the
99
+ ## default #each just calls next over and over.
95
100
  def each
96
101
  self.cur_offset ||= start_offset
97
102
  until done?
98
- n, labels = self.next
99
- raise "no message" unless n
100
- yield n, labels
103
+ offset, labels = self.next
104
+ yield offset, labels
101
105
  end
102
106
  end
103
107
 
104
- ## read a raw email header from a filehandle (or anything that responds to
105
- ## #gets), and turn it into a hash of key-value pairs.
108
+ ## utility method to read a raw email header from an IO stream and turn it
109
+ ## into a hash of key-value pairs. minor special semantics for certain headers.
106
110
  ##
107
- ## WARNING! THIS IS A SPEED-CRITICAL SECTION. Everything you do here will have
108
- ## a significant effect on Sup's processing speed of email from ALL sources.
111
+ ## THIS IS A SPEED-CRITICAL SECTION. Everything you do here will have a
112
+ ## significant effect on Sup's processing speed of email from ALL sources.
109
113
  ## Little things like string interpolation, regexp interpolation, += vs <<,
110
114
  ## all have DRAMATIC effects. BE CAREFUL WHAT YOU DO!
111
115
  def self.parse_raw_email_header f
@@ -116,9 +120,11 @@ class Source
116
120
  case line
117
121
  ## these three can occur multiple times, and we want the first one
118
122
  when /^(Delivered-To|X-Original-To|Envelope-To):\s*(.*?)\s*$/i; header[last = $1.downcase] ||= $2
119
- ## mark this guy specially. not sure why i care.
123
+ ## regular header: overwrite (not that we should see more than one)
124
+ ## TODO: figure out whether just using the first occurrence changes
125
+ ## anything (which would simplify the logic slightly)
120
126
  when /^([^:\s]+):\s*(.*?)\s*$/i; header[last = $1.downcase] = $2
121
- when /^\r*$/; break
127
+ when /^\r*$/; break # blank line signifies end of header
122
128
  else
123
129
  if last
124
130
  header[last] << " " unless header[last].empty?
@@ -133,7 +139,7 @@ class Source
133
139
  header[k] = begin
134
140
  Rfc2047.decode_to $encoding, v
135
141
  rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
136
- #Redwood::log "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
142
+ #debug "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
137
143
  v
138
144
  end
139
145
  end
@@ -144,7 +150,7 @@ protected
144
150
 
145
151
  ## convenience function
146
152
  def parse_raw_email_header f; self.class.parse_raw_email_header f end
147
-
153
+
148
154
  def Source.expand_filesystem_uri uri
149
155
  uri.gsub "~", File.expand_path("~")
150
156
  end
@@ -155,4 +161,75 @@ protected
155
161
  end
156
162
  end
157
163
 
164
+ ## if you have a @labels instance variable, include this
165
+ ## to serialize them nicely as an array, rather than as a
166
+ ## nasty set.
167
+ module SerializeLabelsNicely
168
+ def before_marshal # can return an object
169
+ c = clone
170
+ c.instance_eval { @labels = @labels.to_a.map { |l| l.to_s } }
171
+ c
172
+ end
173
+
174
+ def after_unmarshal!
175
+ @labels = Set.new(@labels.map { |s| s.to_sym })
176
+ end
177
+ end
178
+
179
+ class SourceManager
180
+ include Singleton
181
+
182
+ def initialize
183
+ @sources = {}
184
+ @sources_dirty = false
185
+ @source_mutex = Monitor.new
186
+ end
187
+
188
+ def [](id)
189
+ @source_mutex.synchronize { @sources[id] }
190
+ end
191
+
192
+ def add_source source
193
+ @source_mutex.synchronize do
194
+ raise "duplicate source!" if @sources.include? source
195
+ @sources_dirty = true
196
+ max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
197
+ source.id ||= (max || 0) + 1
198
+ ##source.id += 1 while @sources.member? source.id
199
+ @sources[source.id] = source
200
+ end
201
+ end
202
+
203
+ def sources
204
+ ## favour the inbox by listing non-archived sources first
205
+ @source_mutex.synchronize { @sources.values }.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
206
+ end
207
+
208
+ def source_for uri; sources.find { |s| s.is_source_for? uri }; end
209
+ def usual_sources; sources.find_all { |s| s.usual? }; end
210
+
211
+ def load_sources fn=Redwood::SOURCE_FN
212
+ source_array = (Redwood::load_yaml_obj(fn) || []).map { |o| Recoverable.new o }
213
+ @source_mutex.synchronize do
214
+ @sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
215
+ @sources_dirty = false
216
+ end
217
+ end
218
+
219
+ def save_sources fn=Redwood::SOURCE_FN
220
+ @source_mutex.synchronize do
221
+ if @sources_dirty || @sources.any? { |id, s| s.dirty? }
222
+ bakfn = fn + ".bak"
223
+ if File.exists? fn
224
+ File.chmod 0600, fn
225
+ FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
226
+ end
227
+ Redwood::save_yaml_obj sources, fn, true
228
+ File.chmod 0600, fn
229
+ end
230
+ @sources_dirty = false
231
+ end
232
+ end
233
+ end
234
+
158
235
  end
@@ -35,9 +35,9 @@ class TextField
35
35
  @completion_block = block
36
36
  @field = Ncurses::Form.new_field 1, @width - question.length, @y, @x + question.length, 256, 0
37
37
  @form = Ncurses::Form.new_form [@field]
38
- @value = default
38
+ @value = default || ''
39
39
  Ncurses::Form.post_form @form
40
- set_cursed_value default if default
40
+ set_cursed_value @value
41
41
  end
42
42
 
43
43
  def position_cursor
@@ -112,11 +112,11 @@ class TextField
112
112
  unless @history.empty?
113
113
  value = get_cursed_value
114
114
  @i ||= @history.size
115
- #Redwood::log "history before #{@history.inspect}"
115
+ #debug "history before #{@history.inspect}"
116
116
  @history[@i] = value #unless value =~ /^\s*$/
117
117
  @i = (@i + (c == Ncurses::KEY_UP ? -1 : 1)) % @history.size
118
118
  @value = @history[@i]
119
- #Redwood::log "history after #{@history.inspect}"
119
+ #debug "history after #{@history.inspect}"
120
120
  set_cursed_value @value
121
121
  Ncurses::Form::REQ_END_FIELD
122
122
  end
@@ -24,6 +24,8 @@
24
24
  ## a faked root object tying them all together into one tree
25
25
  ## structure.
26
26
 
27
+ require 'set'
28
+
27
29
  module Redwood
28
30
 
29
31
  class Thread
@@ -101,17 +103,16 @@ class Thread
101
103
  def toggle_label label
102
104
  if has_label? label
103
105
  remove_label label
104
- return false
106
+ false
105
107
  else
106
108
  apply_label label
107
- return true
109
+ true
108
110
  end
109
111
  end
110
112
 
111
113
  def set_labels l; each { |m, *o| m && m.labels = l }; end
112
-
113
114
  def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
114
- def save index; each { |m, *o| m && m.save(index) }; end
115
+ def save_state index; each { |m, *o| m && m.save_state(index) }; end
115
116
 
116
117
  def direct_participants
117
118
  map { |m, *o| [m.from] + m.to if m }.flatten.compact.uniq
@@ -123,15 +124,14 @@ class Thread
123
124
 
124
125
  def size; map { |m, *o| m ? 1 : 0 }.sum; end
125
126
  def subj; argfind { |m, *o| m && m.subj }; end
126
- def labels
127
- map { |m, *o| m && m.labels }.flatten.compact.uniq.sort_by { |t| t.to_s }
128
- end
127
+ def labels; inject(Set.new) { |s, (m, *o)| m ? s | m.labels : s } end
129
128
  def labels= l
130
- each { |m, *o| m && m.labels = l.clone }
129
+ raise ArgumentError, "not a set" unless l.is_a?(Set)
130
+ each { |m, *o| m && m.labels = l.dup }
131
131
  end
132
132
 
133
133
  def latest_message
134
- inject(nil) do |a, b|
134
+ inject(nil) do |a, b|
135
135
  b = b.first
136
136
  if a.nil?
137
137
  b
@@ -162,7 +162,7 @@ class Container
162
162
  @id = id
163
163
  @message, @parent, @thread = nil, nil, nil
164
164
  @children = []
165
- end
165
+ end
166
166
 
167
167
  def each_with_stuff parent=nil
168
168
  yield self, 0, parent
@@ -310,13 +310,15 @@ class ThreadSet
310
310
  private :prune_thread_of
311
311
 
312
312
  def remove_id mid
313
- return unless(c = @messages[mid])
313
+ return unless @messages.member?(mid)
314
+ c = @messages[mid]
314
315
  remove_container c
315
316
  prune_thread_of c
316
317
  end
317
318
 
318
319
  def remove_thread_containing_id mid
319
- c = @messages[mid] or return
320
+ return unless @messages.member?(mid)
321
+ c = @messages[mid]
320
322
  t = c.root.thread
321
323
  @threads.delete_if { |key, thread| t == thread }
322
324
  end
@@ -355,7 +357,7 @@ class ThreadSet
355
357
  return if threads.size < 2
356
358
 
357
359
  containers = threads.map do |t|
358
- c = @messages[t.first.id]
360
+ c = @messages.member?(t.first.id) ? @messages[t.first.id] : nil
359
361
  raise "not in threadset: #{t.first.id}" unless c && c.message
360
362
  c
361
363
  end
@@ -12,7 +12,6 @@ class UndoManager
12
12
 
13
13
  def initialize
14
14
  @@actionlist = []
15
- self.class.i_am_the_instance self
16
15
  end
17
16
 
18
17
  def register desc, *actions, &b
@@ -16,7 +16,6 @@ class UpdateManager
16
16
 
17
17
  def initialize
18
18
  @targets = {}
19
- self.class.i_am_the_instance self
20
19
  end
21
20
 
22
21
  def register o; @targets[o] = true; end
@@ -2,6 +2,7 @@ require 'thread'
2
2
  require 'lockfile'
3
3
  require 'mime/types'
4
4
  require 'pathname'
5
+ require 'set'
5
6
 
6
7
  ## time for some monkeypatching!
7
8
  class Lockfile
@@ -24,6 +25,7 @@ class Lockfile
24
25
  def lockinfo_on_disk
25
26
  h = load_lock_id IO.read(path)
26
27
  h['mtime'] = File.mtime path
28
+ h['path'] = path
27
29
  h
28
30
  end
29
31
 
@@ -90,7 +92,7 @@ end
90
92
 
91
93
  class Range
92
94
  ## only valid for integer ranges (unless I guess it's exclusive)
93
- def size
95
+ def size
94
96
  last - first + (exclude_end? ? 0 : 1)
95
97
  end
96
98
  end
@@ -133,8 +135,8 @@ class Object
133
135
  ## clone of java-style whole-method synchronization
134
136
  ## assumes a @mutex variable
135
137
  ## TODO: clean up, try harder to avoid namespace collisions
136
- def synchronized *meth
137
- meth.each do
138
+ def synchronized *methods
139
+ methods.each do |meth|
138
140
  class_eval <<-EOF
139
141
  alias unsynchronized_#{meth} #{meth}
140
142
  def #{meth}(*a, &b)
@@ -144,8 +146,8 @@ class Object
144
146
  end
145
147
  end
146
148
 
147
- def ignore_concurrent_calls *meth
148
- meth.each do
149
+ def ignore_concurrent_calls *methods
150
+ methods.each do |meth|
149
151
  mutex = "@__concurrent_protector_#{meth}"
150
152
  flag = "@__concurrent_flag_#{meth}"
151
153
  oldmeth = "__unprotected_#{meth}"
@@ -175,7 +177,7 @@ class String
175
177
  ## nasty multibyte hack for ruby 1.8. if it's utf-8, split into chars using
176
178
  ## the utf8 regex and count those. otherwise, use the byte length.
177
179
  def display_length
178
- if $encoding == "UTF-8"
180
+ if $encoding == "UTF-8" || $encoding == "utf8"
179
181
  scan(/./u).size
180
182
  else
181
183
  size
@@ -213,10 +215,10 @@ class String
213
215
  region_start = 0
214
216
  while pos <= length
215
217
  newpos = case state
216
- when :escaped_instring, :escaped_outstring: pos
218
+ when :escaped_instring, :escaped_outstring then pos
217
219
  else index(/[,"\\]/, pos)
218
- end
219
-
220
+ end
221
+
220
222
  if newpos
221
223
  char = self[newpos]
222
224
  else
@@ -227,26 +229,26 @@ class String
227
229
  case char
228
230
  when ?"
229
231
  state = case state
230
- when :outstring: :instring
231
- when :instring: :outstring
232
- when :escaped_instring: :instring
233
- when :escaped_outstring: :outstring
232
+ when :outstring then :instring
233
+ when :instring then :outstring
234
+ when :escaped_instring then :instring
235
+ when :escaped_outstring then :outstring
234
236
  end
235
237
  when ?,, nil
236
238
  state = case state
237
- when :outstring, :escaped_outstring:
239
+ when :outstring, :escaped_outstring then
238
240
  ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
239
241
  region_start = newpos + 1
240
242
  :outstring
241
- when :instring: :instring
242
- when :escaped_instring: :instring
243
+ when :instring then :instring
244
+ when :escaped_instring then :instring
243
245
  end
244
246
  when ?\\
245
247
  state = case state
246
- when :instring: :escaped_instring
247
- when :outstring: :escaped_outstring
248
- when :escaped_instring: :instring
249
- when :escaped_outstring: :outstring
248
+ when :instring then :escaped_instring
249
+ when :outstring then :escaped_outstring
250
+ when :escaped_instring then :instring
251
+ when :escaped_outstring then :outstring
250
252
  end
251
253
  end
252
254
  pos = newpos + 1
@@ -282,10 +284,18 @@ class String
282
284
  gsub(/\t/, " ").gsub(/\r/, "")
283
285
  end
284
286
 
285
- ## takes a space-separated list of words, and returns an array of symbols.
286
- ## typically used in Sup for translating Ferret's representation of a list
287
- ## of labels (a string) to an array of label symbols.
288
- def symbolistize; split.map { |x| x.intern } end
287
+ unless method_defined? :ord
288
+ def ord
289
+ self[0]
290
+ end
291
+ end
292
+
293
+ ## takes a list of words, and returns an array of symbols. typically used in
294
+ ## Sup for translating Ferret's representation of a list of labels (a string)
295
+ ## to an array of label symbols.
296
+ ##
297
+ ## split_on will be passed to String#split, so you can leave this nil for space.
298
+ def to_set_of_symbols split_on=nil; Set.new split(split_on).map { |x| x.strip.intern } end
289
299
  end
290
300
 
291
301
  class Numeric
@@ -413,10 +423,6 @@ class Array
413
423
 
414
424
  def last= e; self[-1] = e end
415
425
  def nonempty?; !empty? end
416
-
417
- def to_set_of_symbols
418
- map { |x| x.is_a?(Symbol) ? x : x.intern }.uniq
419
- end
420
426
  end
421
427
 
422
428
  class Time
@@ -490,19 +496,20 @@ class Time
490
496
  end
491
497
  end
492
498
 
493
- ## simple singleton module. far less complete and insane than the ruby
494
- ## standard library one, but automatically forwards methods calls and
495
- ## allows for constructors that take arguments.
499
+ ## simple singleton module. far less complete and insane than the ruby standard
500
+ ## library one, but it automatically forwards methods calls and allows for
501
+ ## constructors that take arguments.
496
502
  ##
497
- ## You must have #initialize call "self.class.i_am_the_instance self"
498
- ## at some point or everything will fail horribly.
503
+ ## classes that inherit this can define initialize. however, you cannot call
504
+ ## .new on the class. To get the instance of the class, call .instance;
505
+ ## to create the instance, call init.
499
506
  module Singleton
500
507
  module ClassMethods
501
508
  def instance; @instance; end
502
509
  def instantiated?; defined?(@instance) && !@instance.nil?; end
503
510
  def deinstantiate!; @instance = nil; end
504
511
  def method_missing meth, *a, &b
505
- raise "no instance defined!" unless defined? @instance
512
+ raise "no #{name} instance defined in method call to #{meth}!" unless defined? @instance
506
513
 
507
514
  ## if we've been deinstantiated, just drop all calls. this is
508
515
  ## useful because threads that might be active during the
@@ -512,13 +519,14 @@ module Singleton
512
519
 
513
520
  @instance.send meth, *a, &b
514
521
  end
515
- def i_am_the_instance o
522
+ def init *args
516
523
  raise "there can be only one! (instance)" if defined? @instance
517
- @instance = o
524
+ @instance = new(*args)
518
525
  end
519
526
  end
520
527
 
521
528
  def self.included klass
529
+ klass.private_class_method :allocate, :new
522
530
  klass.extend ClassMethods
523
531
  end
524
532
  end
@@ -537,7 +545,7 @@ class Recoverable
537
545
  def has_errors?; !@error.nil?; end
538
546
 
539
547
  def method_missing m, *a, &b; __pass m, *a, &b end
540
-
548
+
541
549
  def id; __pass :id; end
542
550
  def to_s; __pass :to_s; end
543
551
  def to_yaml x; __pass :to_yaml, x; end
@@ -636,17 +644,17 @@ class Iconv
636
644
  def self.easy_decode target, charset, text
637
645
  return text if charset =~ /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i
638
646
  charset = case charset
639
- when /UTF[-_ ]?8/i: "utf-8"
640
- when /(iso[-_ ])?latin[-_ ]?1$/i: "ISO-8859-1"
641
- when /iso[-_ ]?8859[-_ ]?15/i: 'ISO-8859-15'
642
- when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i: "utf-7"
647
+ when /UTF[-_ ]?8/i then "utf-8"
648
+ when /(iso[-_ ])?latin[-_ ]?1$/i then "ISO-8859-1"
649
+ when /iso[-_ ]?8859[-_ ]?15/i then 'ISO-8859-15'
650
+ when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i then "utf-7"
643
651
  else charset
644
652
  end
645
653
 
646
654
  begin
647
655
  Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
648
- rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
649
- Redwood::log "warning: error (#{e.class.name}) decoding text from #{charset} to #{target}: #{text[0 ... 20]}"
656
+ rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::InvalidCharacter, Iconv::IllegalSequence => e
657
+ warn "couldn't transcode text from #{charset} to #{target} (\"#{text[0 ... 20]}\"...) (got #{e.message}); using original as is"
650
658
  text
651
659
  end
652
660
  end