sup 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +12 -0
  4. data/CONTRIBUTORS +84 -0
  5. data/Gemfile +3 -0
  6. data/HACKING +42 -0
  7. data/History.txt +361 -0
  8. data/LICENSE +280 -0
  9. data/README.md +70 -0
  10. data/Rakefile +12 -0
  11. data/ReleaseNotes +231 -0
  12. data/bin/sup +434 -0
  13. data/bin/sup-add +118 -0
  14. data/bin/sup-config +243 -0
  15. data/bin/sup-dump +43 -0
  16. data/bin/sup-import-dump +101 -0
  17. data/bin/sup-psych-ify-config-files +21 -0
  18. data/bin/sup-recover-sources +87 -0
  19. data/bin/sup-sync +210 -0
  20. data/bin/sup-sync-back-maildir +127 -0
  21. data/bin/sup-tweak-labels +140 -0
  22. data/contrib/colorpicker.rb +100 -0
  23. data/contrib/completion/_sup.zsh +114 -0
  24. data/devel/console.sh +3 -0
  25. data/devel/count-loc.sh +3 -0
  26. data/devel/load-index.rb +9 -0
  27. data/devel/profile.rb +12 -0
  28. data/devel/start-console.rb +5 -0
  29. data/doc/FAQ.txt +119 -0
  30. data/doc/Hooks.txt +79 -0
  31. data/doc/Philosophy.txt +69 -0
  32. data/lib/sup.rb +467 -0
  33. data/lib/sup/account.rb +90 -0
  34. data/lib/sup/buffer.rb +768 -0
  35. data/lib/sup/colormap.rb +239 -0
  36. data/lib/sup/contact.rb +67 -0
  37. data/lib/sup/crypto.rb +461 -0
  38. data/lib/sup/draft.rb +119 -0
  39. data/lib/sup/hook.rb +159 -0
  40. data/lib/sup/horizontal_selector.rb +59 -0
  41. data/lib/sup/idle.rb +42 -0
  42. data/lib/sup/index.rb +882 -0
  43. data/lib/sup/interactive_lock.rb +89 -0
  44. data/lib/sup/keymap.rb +140 -0
  45. data/lib/sup/label.rb +87 -0
  46. data/lib/sup/logger.rb +77 -0
  47. data/lib/sup/logger/singleton.rb +10 -0
  48. data/lib/sup/maildir.rb +257 -0
  49. data/lib/sup/mbox.rb +187 -0
  50. data/lib/sup/message.rb +803 -0
  51. data/lib/sup/message_chunks.rb +328 -0
  52. data/lib/sup/mode.rb +140 -0
  53. data/lib/sup/modes/buffer_list_mode.rb +50 -0
  54. data/lib/sup/modes/completion_mode.rb +55 -0
  55. data/lib/sup/modes/compose_mode.rb +38 -0
  56. data/lib/sup/modes/console_mode.rb +125 -0
  57. data/lib/sup/modes/contact_list_mode.rb +148 -0
  58. data/lib/sup/modes/edit_message_async_mode.rb +110 -0
  59. data/lib/sup/modes/edit_message_mode.rb +728 -0
  60. data/lib/sup/modes/file_browser_mode.rb +109 -0
  61. data/lib/sup/modes/forward_mode.rb +82 -0
  62. data/lib/sup/modes/help_mode.rb +19 -0
  63. data/lib/sup/modes/inbox_mode.rb +85 -0
  64. data/lib/sup/modes/label_list_mode.rb +138 -0
  65. data/lib/sup/modes/label_search_results_mode.rb +38 -0
  66. data/lib/sup/modes/line_cursor_mode.rb +203 -0
  67. data/lib/sup/modes/log_mode.rb +57 -0
  68. data/lib/sup/modes/person_search_results_mode.rb +12 -0
  69. data/lib/sup/modes/poll_mode.rb +19 -0
  70. data/lib/sup/modes/reply_mode.rb +228 -0
  71. data/lib/sup/modes/resume_mode.rb +52 -0
  72. data/lib/sup/modes/scroll_mode.rb +252 -0
  73. data/lib/sup/modes/search_list_mode.rb +204 -0
  74. data/lib/sup/modes/search_results_mode.rb +59 -0
  75. data/lib/sup/modes/text_mode.rb +76 -0
  76. data/lib/sup/modes/thread_index_mode.rb +1033 -0
  77. data/lib/sup/modes/thread_view_mode.rb +941 -0
  78. data/lib/sup/person.rb +134 -0
  79. data/lib/sup/poll.rb +272 -0
  80. data/lib/sup/rfc2047.rb +56 -0
  81. data/lib/sup/search.rb +110 -0
  82. data/lib/sup/sent.rb +58 -0
  83. data/lib/sup/service/label_service.rb +45 -0
  84. data/lib/sup/source.rb +244 -0
  85. data/lib/sup/tagger.rb +50 -0
  86. data/lib/sup/textfield.rb +253 -0
  87. data/lib/sup/thread.rb +452 -0
  88. data/lib/sup/time.rb +93 -0
  89. data/lib/sup/undo.rb +38 -0
  90. data/lib/sup/update.rb +30 -0
  91. data/lib/sup/util.rb +747 -0
  92. data/lib/sup/util/ncurses.rb +274 -0
  93. data/lib/sup/util/path.rb +9 -0
  94. data/lib/sup/util/query.rb +17 -0
  95. data/lib/sup/util/uri.rb +15 -0
  96. data/lib/sup/version.rb +3 -0
  97. data/sup.gemspec +53 -0
  98. data/test/dummy_source.rb +61 -0
  99. data/test/gnupg_test_home/gpg.conf +1 -0
  100. data/test/gnupg_test_home/pubring.gpg +0 -0
  101. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  102. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  103. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  104. data/test/gnupg_test_home/secring.gpg +0 -0
  105. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
  106. data/test/gnupg_test_home/trustdb.gpg +0 -0
  107. data/test/integration/test_label_service.rb +18 -0
  108. data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
  109. data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
  110. data/test/messages/missing-line.eml +9 -0
  111. data/test/test_crypto.rb +109 -0
  112. data/test/test_header_parsing.rb +168 -0
  113. data/test/test_helper.rb +7 -0
  114. data/test/test_message.rb +532 -0
  115. data/test/test_messages_dir.rb +147 -0
  116. data/test/test_yaml_migration.rb +85 -0
  117. data/test/test_yaml_regressions.rb +17 -0
  118. data/test/unit/service/test_label_service.rb +19 -0
  119. data/test/unit/test_horizontal_selector.rb +40 -0
  120. data/test/unit/util/test_query.rb +46 -0
  121. data/test/unit/util/test_string.rb +57 -0
  122. data/test/unit/util/test_uri.rb +19 -0
  123. metadata +423 -0
@@ -0,0 +1,93 @@
1
+ class Time
2
+
3
+ Redwood::HookManager.register "time-to-nice-string", <<EOS
4
+ Formats time nicely as string.
5
+ Variables:
6
+ time: The Time instance to be formatted.
7
+ from: The Time instance providing the reference point (considered "now").
8
+ EOS
9
+
10
+ def to_indexable_s
11
+ sprintf "%012d", self
12
+ end
13
+
14
+ def nearest_hour
15
+ if min < 30
16
+ self
17
+ else
18
+ self + (60 - min) * 60
19
+ end
20
+ end
21
+
22
+ def midnight # within a second
23
+ self - (hour * 60 * 60) - (min * 60) - sec
24
+ end
25
+
26
+ def is_the_same_day? other
27
+ (midnight - other.midnight).abs < 1
28
+ end
29
+
30
+ def is_the_day_before? other
31
+ other.midnight - midnight <= 24 * 60 * 60 + 1
32
+ end
33
+
34
+ def to_nice_distance_s from=Time.now
35
+ later_than = (self < from)
36
+ diff = (self.to_i - from.to_i).abs.to_f
37
+ text =
38
+ [ ["second", 60],
39
+ ["minute", 60],
40
+ ["hour", 24],
41
+ ["day", 7],
42
+ ["week", 4.345], # heh heh
43
+ ["month", 12],
44
+ ["year", nil],
45
+ ].argfind do |unit, size|
46
+ if diff.round <= 1
47
+ "one #{unit}"
48
+ elsif size.nil? || diff.round < size
49
+ "#{diff.round} #{unit}s"
50
+ else
51
+ diff /= size.to_f
52
+ false
53
+ end
54
+ end
55
+ if later_than
56
+ text + " ago"
57
+ else
58
+ "in " + text
59
+ end
60
+ end
61
+
62
+ TO_NICE_S_MAX_LEN = 9 # e.g. "Yest.10am"
63
+
64
+ ## This is how a thread date is displayed in thread-index-mode
65
+ def to_nice_s from=Time.now
66
+ Redwood::HookManager.run("time-to-nice-string", :time => self, :from => from) || default_to_nice_s(from)
67
+ end
68
+
69
+ def default_to_nice_s from=Time.now
70
+ if year != from.year
71
+ strftime "%b %Y"
72
+ elsif month != from.month
73
+ strftime "%b %e"
74
+ else
75
+ if is_the_same_day? from
76
+ format = $config[:time_mode] == "24h" ? "%k:%M" : "%l:%M%p"
77
+ strftime(format).downcase
78
+ elsif is_the_day_before? from
79
+ format = $config[:time_mode] == "24h" ? "%kh" : "%l%p"
80
+ "Yest." + nearest_hour.strftime(format).downcase
81
+ else
82
+ strftime "%b %e"
83
+ end
84
+ end
85
+ end
86
+
87
+ ## This is how a message date is displayed in thread-view-mode
88
+ def to_message_nice_s from=Time.now
89
+ format = $config[:time_mode] == "24h" ? "%B %e %Y %k:%M" : "%B %e %Y %l:%M%p"
90
+ strftime format
91
+ end
92
+ end
93
+
@@ -0,0 +1,38 @@
1
+ module Redwood
2
+
3
+ ## Implements a single undo list for the Sup instance
4
+ ##
5
+ ## The basic idea is to keep a list of lambdas to undo
6
+ ## things. When an action is called (such as 'archive'),
7
+ ## a lambda is registered with UndoManager that will
8
+ ## undo the archival action
9
+
10
+ class UndoManager
11
+ include Redwood::Singleton
12
+
13
+ def initialize
14
+ @@actionlist = []
15
+ end
16
+
17
+ def register desc, *actions, &b
18
+ actions = [*actions.flatten]
19
+ actions << b if b
20
+ raise ArgumentError, "need at least one action" unless actions.length > 0
21
+ @@actionlist.push :desc => desc, :actions => actions
22
+ end
23
+
24
+ def undo
25
+ unless @@actionlist.empty?
26
+ actionset = @@actionlist.pop
27
+ actionset[:actions].each { |action| action.call }
28
+ BufferManager.flash "undid #{actionset[:desc]}"
29
+ else
30
+ BufferManager.flash "nothing more to undo!"
31
+ end
32
+ end
33
+
34
+ def clear
35
+ @@actionlist = []
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ module Redwood
2
+
3
+ ## Classic listener/broadcaster paradigm. Handles communication between various
4
+ ## parts of Sup.
5
+ ##
6
+ ## Usage note: don't pass threads around. Neither thread nor message equality is
7
+ ## defined anywhere in Sup beyond standard object equality. To communicate
8
+ ## something about a particular thread, just pass a representative message from
9
+ ## it around.
10
+ ##
11
+ ## (This assumes that no message will be a part of more than one thread within a
12
+ ## single "view". Luckily, that's true.)
13
+
14
+ class UpdateManager
15
+ include Redwood::Singleton
16
+
17
+ def initialize
18
+ @targets = {}
19
+ end
20
+
21
+ def register o; @targets[o] = true; end
22
+ def unregister o; @targets.delete o; end
23
+
24
+ def relay sender, type, *args
25
+ meth = "handle_#{type}_update".intern
26
+ @targets.keys.each { |o| o.send meth, sender, *args unless o == sender if o.respond_to? meth }
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,747 @@
1
+ # encoding: utf-8
2
+
3
+ require 'thread'
4
+ require 'lockfile'
5
+ require 'mime/types'
6
+ require 'pathname'
7
+ require 'set'
8
+ require 'enumerator'
9
+ require 'benchmark'
10
+ require 'unicode'
11
+ require 'fileutils'
12
+
13
+ ## time for some monkeypatching!
14
+ class Symbol
15
+ unless method_defined? :to_proc
16
+ def to_proc
17
+ proc { |obj, *args| obj.send(self, *args) }
18
+ end
19
+ end
20
+ end
21
+
22
+ class Lockfile
23
+ def gen_lock_id
24
+ Hash[
25
+ 'host' => "#{ Socket.gethostname }",
26
+ 'pid' => "#{ Process.pid }",
27
+ 'ppid' => "#{ Process.ppid }",
28
+ 'time' => timestamp,
29
+ 'pname' => $0,
30
+ 'user' => ENV["USER"]
31
+ ]
32
+ end
33
+
34
+ def dump_lock_id lock_id = @lock_id
35
+ "host: %s\npid: %s\nppid: %s\ntime: %s\nuser: %s\npname: %s\n" %
36
+ lock_id.values_at('host','pid','ppid','time','user', 'pname')
37
+ end
38
+
39
+ def lockinfo_on_disk
40
+ h = load_lock_id IO.read(path)
41
+ h['mtime'] = File.mtime path
42
+ h['path'] = path
43
+ h
44
+ end
45
+
46
+ def touch_yourself; touch path end
47
+ end
48
+
49
+ class File
50
+ # platform safe file.link which attempts a copy if hard-linking fails
51
+ def self.safe_link src, dest
52
+ begin
53
+ File.link src, dest
54
+ rescue
55
+ FileUtils.copy src, dest
56
+ end
57
+ end
58
+ end
59
+
60
+ class Pathname
61
+ def human_size
62
+ s =
63
+ begin
64
+ size
65
+ rescue SystemCallError
66
+ return "?"
67
+ end
68
+ s.to_human_size
69
+ end
70
+
71
+ def human_time
72
+ begin
73
+ ctime.strftime("%Y-%m-%d %H:%M")
74
+ rescue SystemCallError
75
+ "?"
76
+ end
77
+ end
78
+ end
79
+
80
+ ## more monkeypatching!
81
+ module RMail
82
+ class EncodingUnsupportedError < StandardError; end
83
+
84
+ class Message
85
+ def self.make_file_attachment fn
86
+ bfn = File.basename fn
87
+ t = MIME::Types.type_for(bfn).first || MIME::Types.type_for("exe").first
88
+ make_attachment IO.read(fn), t.content_type, t.encoding, bfn.to_s
89
+ end
90
+
91
+ def charset
92
+ if header.field?("content-type") && header.fetch("content-type") =~ /charset="?(.*?)"?(;|$)/i
93
+ $1
94
+ end
95
+ end
96
+
97
+ def self.make_attachment payload, mime_type, encoding, filename
98
+ a = Message.new
99
+ a.header.add "Content-Disposition", "attachment; filename=#{filename.inspect}"
100
+ a.header.add "Content-Type", "#{mime_type}; name=#{filename.inspect}"
101
+ a.header.add "Content-Transfer-Encoding", encoding if encoding
102
+ a.body =
103
+ case encoding
104
+ when "base64"
105
+ [payload].pack "m"
106
+ when "quoted-printable"
107
+ [payload].pack "M"
108
+ when "7bit", "8bit", nil
109
+ payload
110
+ else
111
+ raise EncodingUnsupportedError, encoding.inspect
112
+ end
113
+ a
114
+ end
115
+ end
116
+
117
+ class Serialize
118
+ ## Don't add MIME-Version headers on serialization. Sup sometimes want's to serialize
119
+ ## message parts where these headers are not needed and messing with the message on
120
+ ## serialization breaks gpg signatures. The commented section shows the original RMail
121
+ ## code.
122
+ def calculate_boundaries(message)
123
+ calculate_boundaries_low(message, [])
124
+ # unless message.header['MIME-Version']
125
+ # message.header['MIME-Version'] = "1.0"
126
+ # end
127
+ end
128
+ end
129
+
130
+ class Header
131
+
132
+ # Convert to ASCII before trying to match with regexp
133
+ class Field
134
+
135
+ class << self
136
+ def parse(field)
137
+ field = field.dup.to_s
138
+ field = field.fix_encoding!.ascii
139
+ if field =~ EXTRACT_FIELD_NAME_RE
140
+ [ $1, $'.chomp ]
141
+ else
142
+ [ "", Field.value_strip(field) ]
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ ## Be more cautious about invalid content-type headers
149
+ ## the original RMail code calls
150
+ ## value.strip.split(/\s*;\s*/)[0].downcase
151
+ ## without checking if split returned an element
152
+
153
+ # This returns the full content type of this message converted to
154
+ # lower case.
155
+ #
156
+ # If there is no content type header, returns the passed block is
157
+ # executed and its return value is returned. If no block is passed,
158
+ # the value of the +default+ argument is returned.
159
+ def content_type(default = nil)
160
+ if value = self['content-type'] and ct = value.strip.split(/\s*;\s*/)[0]
161
+ return ct.downcase
162
+ else
163
+ if block_given?
164
+ yield
165
+ else
166
+ default
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ class Range
174
+ ## only valid for integer ranges (unless I guess it's exclusive)
175
+ def size
176
+ last - first + (exclude_end? ? 0 : 1)
177
+ end
178
+ end
179
+
180
+ class Module
181
+ def bool_reader *args
182
+ args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
183
+ end
184
+ def bool_writer *args; attr_writer(*args); end
185
+ def bool_accessor *args
186
+ bool_reader(*args)
187
+ bool_writer(*args)
188
+ end
189
+
190
+ def defer_all_other_method_calls_to obj
191
+ class_eval %{
192
+ def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
193
+ def respond_to?(m, include_private = false)
194
+ @#{obj}.respond_to?(m, include_private)
195
+ end
196
+ }
197
+ end
198
+ end
199
+
200
+ class Object
201
+ def ancestors
202
+ ret = []
203
+ klass = self.class
204
+
205
+ until klass == Object
206
+ ret << klass
207
+ klass = klass.superclass
208
+ end
209
+ ret
210
+ end
211
+
212
+ ## "k combinator"
213
+ def returning x; yield x; x; end
214
+
215
+ unless method_defined? :tap
216
+ def tap; yield self; self; end
217
+ end
218
+
219
+ ## clone of java-style whole-method synchronization
220
+ ## assumes a @mutex variable
221
+ ## TODO: clean up, try harder to avoid namespace collisions
222
+ def synchronized *methods
223
+ methods.each do |meth|
224
+ class_eval <<-EOF
225
+ alias unsynchronized_#{meth} #{meth}
226
+ def #{meth}(*a, &b)
227
+ @mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
228
+ end
229
+ EOF
230
+ end
231
+ end
232
+
233
+ def ignore_concurrent_calls *methods
234
+ methods.each do |meth|
235
+ mutex = "@__concurrent_protector_#{meth}"
236
+ flag = "@__concurrent_flag_#{meth}"
237
+ oldmeth = "__unprotected_#{meth}"
238
+ class_eval <<-EOF
239
+ alias #{oldmeth} #{meth}
240
+ def #{meth}(*a, &b)
241
+ #{mutex} = Mutex.new unless defined? #{mutex}
242
+ #{flag} = true unless defined? #{flag}
243
+ run = #{mutex}.synchronize do
244
+ if #{flag}
245
+ #{flag} = false
246
+ true
247
+ end
248
+ end
249
+ if run
250
+ ret = #{oldmeth}(*a, &b)
251
+ #{mutex}.synchronize { #{flag} = true }
252
+ ret
253
+ end
254
+ end
255
+ EOF
256
+ end
257
+ end
258
+
259
+ def benchmark s, &b
260
+ ret = nil
261
+ times = Benchmark.measure { ret = b.call }
262
+ debug "benchmark #{s}: #{times}"
263
+ ret
264
+ end
265
+ end
266
+
267
+ class String
268
+ def display_length
269
+ @display_length ||= Unicode.width(self.fix_encoding!, false)
270
+
271
+ # if Unicode.width fails and returns -1, fall back to
272
+ # regular String#length, see pull-request: #256.
273
+ if @display_length < 0
274
+ @display_length = self.length
275
+ end
276
+
277
+ @display_length
278
+ end
279
+
280
+ def slice_by_display_length len
281
+ each_char.each_with_object "" do |c, buffer|
282
+ len -= c.display_length
283
+ buffer << c if len >= 0
284
+ end
285
+ end
286
+
287
+ def camel_to_hyphy
288
+ self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
289
+ end
290
+
291
+ def find_all_positions x
292
+ ret = []
293
+ start = 0
294
+ while start < length
295
+ pos = index x, start
296
+ break if pos.nil?
297
+ ret << pos
298
+ start = pos + 1
299
+ end
300
+ ret
301
+ end
302
+
303
+ ## a very complicated regex found on teh internets to split on
304
+ ## commas, unless they occurr within double quotes.
305
+ def split_on_commas
306
+ normalize_whitespace().split(/,\s*(?=(?:[^"]*"[^"]*")*(?![^"]*"))/)
307
+ end
308
+
309
+ ## ok, here we do it the hard way. got to have a remainder for purposes of
310
+ ## tab-completing full email addresses
311
+ def split_on_commas_with_remainder
312
+ ret = []
313
+ state = :outstring
314
+ pos = 0
315
+ region_start = 0
316
+ while pos <= length
317
+ newpos = case state
318
+ when :escaped_instring, :escaped_outstring then pos
319
+ else index(/[,"\\]/, pos)
320
+ end
321
+
322
+ if newpos
323
+ char = self[newpos]
324
+ else
325
+ char = nil
326
+ newpos = length
327
+ end
328
+
329
+ case char
330
+ when ?"
331
+ state = case state
332
+ when :outstring then :instring
333
+ when :instring then :outstring
334
+ when :escaped_instring then :instring
335
+ when :escaped_outstring then :outstring
336
+ end
337
+ when ?,, nil
338
+ state = case state
339
+ when :outstring, :escaped_outstring then
340
+ ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
341
+ region_start = newpos + 1
342
+ :outstring
343
+ when :instring then :instring
344
+ when :escaped_instring then :instring
345
+ end
346
+ when ?\\
347
+ state = case state
348
+ when :instring then :escaped_instring
349
+ when :outstring then :escaped_outstring
350
+ when :escaped_instring then :instring
351
+ when :escaped_outstring then :outstring
352
+ end
353
+ end
354
+ pos = newpos + 1
355
+ end
356
+
357
+ remainder = case state
358
+ when :instring
359
+ self[region_start .. -1].gsub(/^\s+/, "")
360
+ else
361
+ nil
362
+ end
363
+
364
+ [ret, remainder]
365
+ end
366
+
367
+ def wrap len
368
+ ret = []
369
+ s = self
370
+ while s.display_length > len
371
+ cut = s.slice_by_display_length(len).rindex(/\s/)
372
+ if cut
373
+ ret << s[0 ... cut]
374
+ s = s[(cut + 1) .. -1]
375
+ else
376
+ ret << s.slice_by_display_length(len)
377
+ s = s[ret.last.length .. -1]
378
+ end
379
+ end
380
+ ret << s
381
+ end
382
+
383
+ # Fix the damn string! make sure it is valid utf-8, then convert to
384
+ # user encoding.
385
+ #
386
+ # Not Ruby 1.8 compatible
387
+ def fix_encoding!
388
+ # first try to encode to utf-8 from whatever current encoding
389
+ encode!('UTF-8', :invalid => :replace, :undef => :replace)
390
+
391
+ # do this anyway in case string is set to be UTF-8, encoding to
392
+ # something else (UTF-16 which can fully represent UTF-8) and back
393
+ # ensures invalid chars are replaced.
394
+ encode!('UTF-16', 'UTF-8', :invalid => :replace, :undef => :replace)
395
+ encode!('UTF-8', 'UTF-16', :invalid => :replace, :undef => :replace)
396
+
397
+ fail "Could not create valid UTF-8 string out of: '#{self.to_s}'." unless valid_encoding?
398
+
399
+ # now convert to $encoding
400
+ encode!($encoding, :invalid => :replace, :undef => :replace)
401
+
402
+ fail "Could not create valid #{$encoding.inspect} string out of: '#{self.to_s}'." unless valid_encoding?
403
+
404
+ self
405
+ end
406
+
407
+ # transcode the string if original encoding is know
408
+ # fix if broken.
409
+ #
410
+ # Not Ruby 1.8 compatible
411
+ def transcode to_encoding, from_encoding
412
+ begin
413
+ encode!(to_encoding, from_encoding, :invalid => :replace, :undef => :replace)
414
+
415
+ unless valid_encoding?
416
+ # fix encoding (through UTF-8)
417
+ encode!('UTF-16', from_encoding, :invalid => :replace, :undef => :replace)
418
+ encode!(to_encoding, 'UTF-16', :invalid => :replace, :undef => :replace)
419
+ end
420
+
421
+ rescue Encoding::ConverterNotFoundError
422
+ debug "Encoding converter not found for #{from_encoding.inspect} or #{to_encoding.inspect}, fixing string: '#{self.to_s}', but expect weird characters."
423
+ fix_encoding!
424
+ end
425
+
426
+ fail "Could not create valid #{to_encoding.inspect} string out of: '#{self.to_s}'." unless valid_encoding?
427
+
428
+ self
429
+ end
430
+
431
+ def normalize_whitespace
432
+ fix_encoding!
433
+ gsub(/\t/, " ").gsub(/\r/, "")
434
+ end
435
+
436
+ unless method_defined? :ord
437
+ def ord
438
+ self[0]
439
+ end
440
+ end
441
+
442
+ unless method_defined? :each
443
+ def each &b
444
+ each_line &b
445
+ end
446
+ end
447
+
448
+ ## takes a list of words, and returns an array of symbols. typically used in
449
+ ## Sup for translating Xapian's representation of a list of labels (a string)
450
+ ## to an array of label symbols.
451
+ ##
452
+ ## split_on will be passed to String#split, so you can leave this nil for space.
453
+ def to_set_of_symbols split_on=nil; Set.new split(split_on).map { |x| x.strip.intern } end
454
+
455
+ class CheckError < ArgumentError; end
456
+ def check
457
+ begin
458
+ fail "unexpected encoding #{encoding}" if respond_to?(:encoding) && !(encoding == Encoding::UTF_8 || encoding == Encoding::ASCII)
459
+ fail "invalid encoding" if respond_to?(:valid_encoding?) && !valid_encoding?
460
+ rescue
461
+ raise CheckError.new($!.message)
462
+ end
463
+ end
464
+
465
+ def ascii
466
+ out = ""
467
+ each_byte do |b|
468
+ if (b & 128) != 0
469
+ out << "\\x#{b.to_s 16}"
470
+ else
471
+ out << b.chr
472
+ end
473
+ end
474
+ out = out.fix_encoding! # this should now be an utf-8 string of ascii
475
+ # compat chars.
476
+ end
477
+
478
+ unless method_defined? :ascii_only?
479
+ def ascii_only?
480
+ size.times { |i| return false if self[i] & 128 != 0 }
481
+ return true
482
+ end
483
+ end
484
+ end
485
+
486
+ class Numeric
487
+ def clamp min, max
488
+ if self < min
489
+ min
490
+ elsif self > max
491
+ max
492
+ else
493
+ self
494
+ end
495
+ end
496
+
497
+ def in? range; range.member? self; end
498
+
499
+ def to_human_size
500
+ if self < 1024
501
+ to_s + "b"
502
+ elsif self < (1024 * 1024)
503
+ (self / 1024).to_s + "k"
504
+ elsif self < (1024 * 1024 * 1024)
505
+ (self / 1024 / 1024).to_s + "m"
506
+ else
507
+ (self / 1024 / 1024 / 1024).to_s + "g"
508
+ end
509
+ end
510
+ end
511
+
512
+ class Fixnum
513
+ def to_character
514
+ if self < 128 && self >= 0
515
+ chr
516
+ else
517
+ "<#{self}>"
518
+ end
519
+ end
520
+
521
+ unless method_defined?(:ord)
522
+ def ord
523
+ self
524
+ end
525
+ end
526
+
527
+ ## hacking the english language
528
+ def pluralize s
529
+ to_s + " " +
530
+ if self == 1
531
+ s
532
+ else
533
+ if s =~ /(.*)y$/
534
+ $1 + "ies"
535
+ else
536
+ s + "s"
537
+ end
538
+ end
539
+ end
540
+ end
541
+
542
+ class Hash
543
+ def - o
544
+ Hash[*self.map { |k, v| [k, v] unless o.include? k }.compact.flatten_one_level]
545
+ end
546
+
547
+ def select_by_value v=true
548
+ select { |k, vv| vv == v }.map { |x| x.first }
549
+ end
550
+ end
551
+
552
+ module Enumerable
553
+ def map_with_index
554
+ ret = []
555
+ each_with_index { |x, i| ret << yield(x, i) }
556
+ ret
557
+ end
558
+
559
+ def sum; inject(0) { |x, y| x + y }; end
560
+
561
+ def map_to_hash
562
+ ret = {}
563
+ each { |x| ret[x] = yield(x) }
564
+ ret
565
+ end
566
+
567
+ # like find, except returns the value of the block rather than the
568
+ # element itself.
569
+ def argfind
570
+ ret = nil
571
+ find { |e| ret ||= yield(e) }
572
+ ret || nil # force
573
+ end
574
+
575
+ def argmin
576
+ best, bestval = nil, nil
577
+ each do |e|
578
+ val = yield e
579
+ if bestval.nil? || val < bestval
580
+ best, bestval = e, val
581
+ end
582
+ end
583
+ best
584
+ end
585
+
586
+ ## returns the maximum shared prefix of an array of strings
587
+ ## optinally excluding a prefix
588
+ def shared_prefix caseless=false, exclude=""
589
+ return "" if empty?
590
+ prefix = ""
591
+ (0 ... first.length).each do |i|
592
+ c = (caseless ? first.downcase : first)[i]
593
+ break unless all? { |s| (caseless ? s.downcase : s)[i] == c }
594
+ next if exclude[i] == c
595
+ prefix += first[i].chr
596
+ end
597
+ prefix
598
+ end
599
+
600
+ def max_of
601
+ map { |e| yield e }.max
602
+ end
603
+
604
+ ## returns all the entries which are equal to startline up to endline
605
+ def between startline, endline
606
+ select { |l| true if l == startline .. l == endline }
607
+ end
608
+ end
609
+
610
+ unless Object.const_defined? :Enumerator
611
+ Enumerator = Enumerable::Enumerator
612
+ end
613
+
614
+ class Array
615
+ def flatten_one_level
616
+ inject([]) { |a, e| a + e }
617
+ end
618
+
619
+ def to_h; Hash[*flatten]; end
620
+ def rest; self[1..-1]; end
621
+
622
+ def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
623
+
624
+ def last= e; self[-1] = e end
625
+ def nonempty?; !empty? end
626
+ end
627
+
628
+ ## simple singleton module. far less complete and insane than the ruby standard
629
+ ## library one, but it automatically forwards methods calls and allows for
630
+ ## constructors that take arguments.
631
+ ##
632
+ ## classes that inherit this can define initialize. however, you cannot call
633
+ ## .new on the class. To get the instance of the class, call .instance;
634
+ ## to create the instance, call init.
635
+ module Redwood
636
+ module Singleton
637
+ module ClassMethods
638
+ def instance; @instance; end
639
+ def instantiated?; defined?(@instance) && !@instance.nil?; end
640
+ def deinstantiate!; @instance = nil; end
641
+ def method_missing meth, *a, &b
642
+ raise "no #{name} instance defined in method call to #{meth}!" unless defined? @instance
643
+
644
+ ## if we've been deinstantiated, just drop all calls. this is
645
+ ## useful because threads that might be active during the
646
+ ## cleanup process (e.g. polling) would otherwise have to
647
+ ## special-case every call to a Singleton object
648
+ return nil if @instance.nil?
649
+
650
+ # Speed up further calls by defining a shortcut around method_missing
651
+ if meth.to_s[-1,1] == '='
652
+ # Argh! Inconsistency! Setters do not work like all the other methods.
653
+ class_eval "def self.#{meth}(a); @instance.send :#{meth}, a; end"
654
+ else
655
+ class_eval "def self.#{meth}(*a, &b); @instance.send :#{meth}, *a, &b; end"
656
+ end
657
+
658
+ @instance.send meth, *a, &b
659
+ end
660
+ def init *args
661
+ raise "there can be only one! (instance)" if instantiated?
662
+ @instance = new(*args)
663
+ end
664
+ end
665
+
666
+ def self.included klass
667
+ klass.private_class_method :allocate, :new
668
+ klass.extend ClassMethods
669
+ end
670
+ end
671
+ end
672
+
673
+ ## acts like a hash with an initialization block, but saves any
674
+ ## newly-created value even upon lookup.
675
+ ##
676
+ ## for example:
677
+ ##
678
+ ## class C
679
+ ## attr_accessor :val
680
+ ## def initialize; @val = 0 end
681
+ ## end
682
+ ##
683
+ ## h = Hash.new { C.new }
684
+ ## h[:a].val # => 0
685
+ ## h[:a].val = 1
686
+ ## h[:a].val # => 0
687
+ ##
688
+ ## h2 = SavingHash.new { C.new }
689
+ ## h2[:a].val # => 0
690
+ ## h2[:a].val = 1
691
+ ## h2[:a].val # => 1
692
+ ##
693
+ ## important note: you REALLY want to use #member? to test existence,
694
+ ## because just checking h[anything] will always evaluate to true
695
+ ## (except for degenerate constructor blocks that return nil or false)
696
+ class SavingHash
697
+ def initialize &b
698
+ @constructor = b
699
+ @hash = Hash.new
700
+ end
701
+
702
+ def [] k
703
+ @hash[k] ||= @constructor.call(k)
704
+ end
705
+
706
+ defer_all_other_method_calls_to :hash
707
+ end
708
+
709
+ class OrderedHash < Hash
710
+ alias_method :store, :[]=
711
+ alias_method :each_pair, :each
712
+ attr_reader :keys
713
+
714
+ def initialize *a
715
+ @keys = []
716
+ a.each { |k, v| self[k] = v }
717
+ end
718
+
719
+ def []= key, val
720
+ @keys << key unless member?(key)
721
+ super
722
+ end
723
+
724
+ def values; keys.map { |k| self[k] } end
725
+ def index key; @keys.index key end
726
+
727
+ def delete key
728
+ @keys.delete key
729
+ super
730
+ end
731
+
732
+ def each; @keys.each { |k| yield k, self[k] } end
733
+ end
734
+
735
+ ## easy thread-safe class for determining who's the "winner" in a race (i.e.
736
+ ## first person to hit the finish line
737
+ class FinishLine
738
+ def initialize
739
+ @m = Mutex.new
740
+ @over = false
741
+ end
742
+
743
+ def winner?
744
+ @m.synchronize { !@over && @over = true }
745
+ end
746
+ end
747
+