sup 0.19.0

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