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.
- data/CONTRIBUTORS +13 -6
- data/History.txt +19 -0
- data/ReleaseNotes +35 -0
- data/bin/sup +82 -77
- data/bin/sup-add +7 -7
- data/bin/sup-config +104 -85
- data/bin/sup-dump +4 -5
- data/bin/sup-recover-sources +9 -10
- data/bin/sup-sync +121 -100
- data/bin/sup-sync-back +18 -15
- data/bin/sup-tweak-labels +24 -21
- data/lib/sup.rb +53 -33
- data/lib/sup/account.rb +0 -2
- data/lib/sup/buffer.rb +47 -22
- data/lib/sup/colormap.rb +6 -6
- data/lib/sup/contact.rb +0 -2
- data/lib/sup/crypto.rb +34 -23
- data/lib/sup/draft.rb +6 -14
- data/lib/sup/ferret_index.rb +471 -0
- data/lib/sup/hook.rb +30 -43
- data/lib/sup/hook.rb.BACKUP.8625.rb +158 -0
- data/lib/sup/hook.rb.BACKUP.8681.rb +158 -0
- data/lib/sup/hook.rb.BASE.8625.rb +155 -0
- data/lib/sup/hook.rb.BASE.8681.rb +155 -0
- data/lib/sup/hook.rb.LOCAL.8625.rb +142 -0
- data/lib/sup/hook.rb.LOCAL.8681.rb +142 -0
- data/lib/sup/hook.rb.REMOTE.8625.rb +145 -0
- data/lib/sup/hook.rb.REMOTE.8681.rb +145 -0
- data/lib/sup/imap.rb +18 -8
- data/lib/sup/index.rb +70 -528
- data/lib/sup/interactive-lock.rb +74 -0
- data/lib/sup/keymap.rb +26 -26
- data/lib/sup/label.rb +2 -4
- data/lib/sup/logger.rb +54 -35
- data/lib/sup/maildir.rb +41 -6
- data/lib/sup/mbox.rb +1 -1
- data/lib/sup/mbox/loader.rb +18 -6
- data/lib/sup/mbox/ssh-file.rb +1 -7
- data/lib/sup/message-chunks.rb +36 -23
- data/lib/sup/message.rb +126 -46
- data/lib/sup/mode.rb +3 -2
- data/lib/sup/modes/console-mode.rb +108 -0
- data/lib/sup/modes/edit-message-mode.rb +15 -5
- data/lib/sup/modes/inbox-mode.rb +2 -4
- data/lib/sup/modes/label-list-mode.rb +1 -1
- data/lib/sup/modes/line-cursor-mode.rb +18 -18
- data/lib/sup/modes/log-mode.rb +29 -16
- data/lib/sup/modes/poll-mode.rb +7 -9
- data/lib/sup/modes/reply-mode.rb +5 -3
- data/lib/sup/modes/scroll-mode.rb +2 -2
- data/lib/sup/modes/search-results-mode.rb +9 -11
- data/lib/sup/modes/text-mode.rb +2 -2
- data/lib/sup/modes/thread-index-mode.rb +26 -16
- data/lib/sup/modes/thread-view-mode.rb +84 -39
- data/lib/sup/person.rb +6 -8
- data/lib/sup/poll.rb +46 -47
- data/lib/sup/rfc2047.rb +1 -5
- data/lib/sup/sent.rb +27 -20
- data/lib/sup/source.rb +90 -13
- data/lib/sup/textfield.rb +4 -4
- data/lib/sup/thread.rb +15 -13
- data/lib/sup/undo.rb +0 -1
- data/lib/sup/update.rb +0 -1
- data/lib/sup/util.rb +51 -43
- data/lib/sup/xapian_index.rb +566 -0
- metadata +57 -46
- data/lib/sup/suicide.rb +0 -36
data/lib/sup/rfc2047.rb
CHANGED
data/lib/sup/sent.rb
CHANGED
@@ -3,28 +3,34 @@ module Redwood
|
|
3
3
|
class SentManager
|
4
4
|
include Singleton
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
attr_reader :source, :source_uri
|
7
|
+
|
8
|
+
def initialize source_uri
|
9
9
|
@source = nil
|
10
|
-
|
10
|
+
@source_uri = source_uri
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
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
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
44
|
-
def
|
45
|
-
|
46
|
-
def
|
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
|
data/lib/sup/source.rb
CHANGED
@@ -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
|
-
|
99
|
-
|
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
|
105
|
-
##
|
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
|
-
##
|
108
|
-
##
|
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
|
-
##
|
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
|
-
#
|
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
|
data/lib/sup/textfield.rb
CHANGED
@@ -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
|
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
|
-
#
|
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
|
-
#
|
119
|
+
#debug "history after #{@history.inspect}"
|
120
120
|
set_cursed_value @value
|
121
121
|
Ncurses::Form::REQ_END_FIELD
|
122
122
|
end
|
data/lib/sup/thread.rb
CHANGED
@@ -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
|
-
|
106
|
+
false
|
105
107
|
else
|
106
108
|
apply_label label
|
107
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
data/lib/sup/undo.rb
CHANGED
data/lib/sup/update.rb
CHANGED
data/lib/sup/util.rb
CHANGED
@@ -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 *
|
137
|
-
|
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 *
|
148
|
-
|
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
|
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
|
231
|
-
when :instring
|
232
|
-
when :escaped_instring
|
233
|
-
when :escaped_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
|
242
|
-
when :escaped_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
|
247
|
-
when :outstring
|
248
|
-
when :escaped_instring
|
249
|
-
when :escaped_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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|
-
##
|
495
|
-
##
|
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
|
-
##
|
498
|
-
##
|
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
|
522
|
+
def init *args
|
516
523
|
raise "there can be only one! (instance)" if defined? @instance
|
517
|
-
@instance =
|
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
|
640
|
-
when /(iso[-_ ])?latin[-_ ]?1$/i
|
641
|
-
when /iso[-_ ]?8859[-_ ]?15/i
|
642
|
-
when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i
|
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
|
-
|
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
|