sup 0.0.6 → 0.0.7
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/HACKING +50 -0
- data/History.txt +12 -0
- data/Manifest.txt +2 -0
- data/Rakefile +5 -1
- data/bin/sup +24 -8
- data/bin/sup-add +106 -0
- data/bin/sup-import +85 -165
- data/doc/FAQ.txt +48 -11
- data/doc/TODO +45 -12
- data/doc/UserGuide.txt +54 -42
- data/lib/sup.rb +6 -2
- data/lib/sup/buffer.rb +20 -4
- data/lib/sup/contact.rb +22 -12
- data/lib/sup/draft.rb +27 -7
- data/lib/sup/imap.rb +107 -97
- data/lib/sup/index.rb +35 -25
- data/lib/sup/label.rb +2 -2
- data/lib/sup/mbox.rb +20 -15
- data/lib/sup/mbox/loader.rb +0 -1
- data/lib/sup/mbox/ssh-file.rb +58 -47
- data/lib/sup/mbox/ssh-loader.rb +13 -20
- data/lib/sup/message.rb +15 -9
- data/lib/sup/mode.rb +16 -6
- data/lib/sup/modes/compose-mode.rb +7 -3
- data/lib/sup/modes/contact-list-mode.rb +50 -25
- data/lib/sup/modes/edit-message-mode.rb +11 -8
- data/lib/sup/modes/inbox-mode.rb +19 -3
- data/lib/sup/modes/label-list-mode.rb +4 -12
- data/lib/sup/modes/log-mode.rb +6 -0
- data/lib/sup/modes/reply-mode.rb +19 -6
- data/lib/sup/modes/resume-mode.rb +13 -9
- data/lib/sup/modes/scroll-mode.rb +13 -2
- data/lib/sup/modes/thread-index-mode.rb +94 -43
- data/lib/sup/modes/thread-view-mode.rb +198 -130
- data/lib/sup/person.rb +1 -1
- data/lib/sup/poll.rb +60 -47
- data/lib/sup/sent.rb +3 -2
- data/lib/sup/source.rb +14 -6
- data/lib/sup/textfield.rb +1 -0
- data/lib/sup/thread.rb +76 -23
- data/lib/sup/update.rb +2 -3
- data/lib/sup/util.rb +13 -13
- metadata +14 -2
data/lib/sup/person.rb
CHANGED
@@ -14,7 +14,7 @@ class PersonManager
|
|
14
14
|
def register email, name
|
15
15
|
return unless name
|
16
16
|
|
17
|
-
name = name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
|
17
|
+
name = name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ").gsub(/^['"]|['"]$/, "")
|
18
18
|
|
19
19
|
## all else being equal, prefer longer names, unless the prior name
|
20
20
|
## doesn't contain any capitalization
|
data/lib/sup/poll.rb
CHANGED
@@ -24,7 +24,7 @@ class PollManager
|
|
24
24
|
if num > 0
|
25
25
|
BufferManager.flash "Loaded #{num} new messages, #{numi} to inbox."
|
26
26
|
else
|
27
|
-
BufferManager.flash "No new messages."
|
27
|
+
BufferManager.flash "No new messages."
|
28
28
|
end
|
29
29
|
[num, numi]
|
30
30
|
end
|
@@ -38,59 +38,27 @@ class PollManager
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
## TODO: merge this with sup-import
|
42
41
|
def do_poll
|
43
42
|
total_num = total_numi = 0
|
44
43
|
@mutex.synchronize do
|
45
|
-
found = {}
|
46
44
|
Index.usual_sources.each do |source|
|
47
|
-
|
48
|
-
|
49
|
-
yield "Loading from #{source}... "
|
50
|
-
start_offset = nil
|
45
|
+
# yield "source #{source} is done? #{source.done?} (cur_offset #{source.cur_offset} >= #{source.end_offset})"
|
46
|
+
yield "Loading from #{source}... " unless source.done? || source.broken?
|
51
47
|
num = 0
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
begin
|
61
|
-
m = Redwood::Message.new :source => source, :source_info => offset, :labels => labels
|
62
|
-
rescue MessageFormatError => e
|
63
|
-
yield "Non-fatal error loading message #{source}##{offset}: #{e.message}"
|
64
|
-
next
|
65
|
-
end
|
66
|
-
|
67
|
-
if found[m.id]
|
68
|
-
yield "Skipping duplicate message #{m.id}"
|
69
|
-
next
|
70
|
-
end
|
71
|
-
found[m.id] = true
|
72
|
-
|
73
|
-
if Index.add_message m
|
74
|
-
UpdateManager.relay :add, m
|
75
|
-
num += 1
|
76
|
-
total_num += 1
|
77
|
-
total_numi += 1 if m.labels.include? :inbox
|
78
|
-
end
|
79
|
-
|
80
|
-
if num % 1000 == 0 && num > 0
|
81
|
-
elapsed = Time.now - start
|
82
|
-
pctdone = source.pct_done
|
83
|
-
remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
|
84
|
-
yield "## #{num} (#{pctdone}% done) read; #{elapsed.to_time_s} elapsed; est. #{remaining.to_time_s} remaining"
|
85
|
-
end
|
86
|
-
rescue SourceError => e
|
87
|
-
msg = "Fatal error loading from #{source}: #{e.message}"
|
88
|
-
Redwood::log msg
|
89
|
-
yield msg
|
90
|
-
break
|
48
|
+
numi = 0
|
49
|
+
add_new_messages_from source do |m, offset, entry|
|
50
|
+
## always preserve the labels on disk.
|
51
|
+
m.labels = entry[:label].split(/\s+/).map { |x| x.intern } if entry
|
52
|
+
yield "Found message at #{offset} with labels {#{m.labels * ', '}}"
|
53
|
+
unless entry
|
54
|
+
num += 1
|
55
|
+
numi += 1 if m.labels.include? :inbox
|
91
56
|
end
|
57
|
+
m
|
92
58
|
end
|
93
|
-
yield "Found #{num} messages" unless num == 0
|
59
|
+
yield "Found #{num} messages, #{numi} to inbox" unless num == 0
|
60
|
+
total_num += num
|
61
|
+
total_numi += numi
|
94
62
|
end
|
95
63
|
|
96
64
|
yield "Done polling; loaded #{total_num} new messages total"
|
@@ -99,6 +67,51 @@ class PollManager
|
|
99
67
|
end
|
100
68
|
[total_num, total_numi]
|
101
69
|
end
|
70
|
+
|
71
|
+
## this is the main mechanism for adding new messages to the
|
72
|
+
## index. it's called both by sup-import and by PollMode.
|
73
|
+
##
|
74
|
+
## for each new message in the source, this yields the message, the
|
75
|
+
## source offset, and the index entry on disk (if any). it expects
|
76
|
+
## the yield to return the message (possibly altered in some way),
|
77
|
+
## and then adds it (if new) or updates it (if previously seen).
|
78
|
+
##
|
79
|
+
## the labels of the yielded message are the source labels. it is
|
80
|
+
## likely that callers will want to replace these with the index
|
81
|
+
## labels, if they exist, so that state is not lost when e.g. a new
|
82
|
+
## version of a message from a mailing list comes in.
|
83
|
+
def add_new_messages_from source
|
84
|
+
return if source.done? || source.broken?
|
85
|
+
|
86
|
+
source.each do |offset, labels|
|
87
|
+
if source.broken?
|
88
|
+
Redwood::log "error loading messages from #{source}: #{source.broken_msg}"
|
89
|
+
return
|
90
|
+
end
|
91
|
+
|
92
|
+
labels.each { |l| LabelManager << l }
|
93
|
+
|
94
|
+
begin
|
95
|
+
m = Message.new :source => source, :source_info => offset, :labels => labels
|
96
|
+
if m.source_marked_read?
|
97
|
+
m.remove_label :unread
|
98
|
+
labels.delete :unread
|
99
|
+
end
|
100
|
+
|
101
|
+
docid, entry = Index.load_entry_for_id m.id
|
102
|
+
m = yield m, offset, entry
|
103
|
+
next unless m
|
104
|
+
if entry
|
105
|
+
Index.update_message m, docid, entry
|
106
|
+
else
|
107
|
+
Index.add_message m
|
108
|
+
UpdateManager.relay self, :add, m
|
109
|
+
end
|
110
|
+
rescue MessageFormatError, SourceError => e
|
111
|
+
Redwood::log "ignoring erroneous message at #{source}##{offset}: #{e.message}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
102
115
|
end
|
103
116
|
|
104
117
|
end
|
data/lib/sup/sent.rb
CHANGED
@@ -10,7 +10,7 @@ class SentManager
|
|
10
10
|
self.class.i_am_the_instance self
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.source_name; "sent
|
13
|
+
def self.source_name; "sup://sent"; end
|
14
14
|
def self.source_id; 9998; end
|
15
15
|
def new_source; @source = SentLoader.new; end
|
16
16
|
|
@@ -24,7 +24,7 @@ class SentManager
|
|
24
24
|
@source.each do |offset, labels|
|
25
25
|
m = Message.new :source => @source, :source_info => offset, :labels => labels
|
26
26
|
Index.add_message m
|
27
|
-
UpdateManager.relay :add, m
|
27
|
+
UpdateManager.relay self, :add, m
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -36,6 +36,7 @@ class SentLoader < MBox::Loader
|
|
36
36
|
super "mbox://" + filename, cur_offset, true, true
|
37
37
|
end
|
38
38
|
|
39
|
+
def uri; SentManager.source_name; end
|
39
40
|
def to_s; SentManager.source_name; end
|
40
41
|
def id; SentManager.source_id; end
|
41
42
|
def labels; [:sent, :inbox]; end
|
data/lib/sup/source.rb
CHANGED
@@ -10,8 +10,8 @@ class Source
|
|
10
10
|
## 3. (optional) see whether the source has marked it read or not
|
11
11
|
##
|
12
12
|
## In particular, Sup doesn't need to move messages, mark them as
|
13
|
-
## read, delete them, or anything else. (Well,
|
14
|
-
##
|
13
|
+
## read, delete them, or anything else. (Well, at some point it will
|
14
|
+
## need to delete them, but that will be an optional capability.)
|
15
15
|
##
|
16
16
|
## On the other hand, Sup assumes that you can assign each message a
|
17
17
|
## unique integer id, such that newer messages have higher ids than
|
@@ -22,16 +22,19 @@ class Source
|
|
22
22
|
## capability, e.g. IMAP, then you have to do a little more work to
|
23
23
|
## simulate it.
|
24
24
|
##
|
25
|
-
## To write a new source, subclass this class, and
|
25
|
+
## To write a new source, subclass this class, and implement:
|
26
26
|
##
|
27
27
|
## - start_offset
|
28
|
-
## - end_offset
|
28
|
+
## - end_offset (exclusive!)
|
29
29
|
## - load_header offset
|
30
30
|
## - load_message offset
|
31
31
|
## - raw_header offset
|
32
|
-
## - raw_full_message offset
|
32
|
+
## - raw_full_message offset
|
33
33
|
## - next (or each, if you prefer)
|
34
34
|
##
|
35
|
+
## ... where "offset" really means unique id. (You can tell I
|
36
|
+
## started with mbox.)
|
37
|
+
##
|
35
38
|
## You can throw SourceErrors from any of those, but we don't catch
|
36
39
|
## anything else, so make sure you catch *all* errors and reraise
|
37
40
|
## them as SourceErrors, and set broken_msg to something if the
|
@@ -39,6 +42,11 @@ class Source
|
|
39
42
|
##
|
40
43
|
## Also, be sure to make the source thread-safe, since it WILL be
|
41
44
|
## pummeled from multiple threads at once.
|
45
|
+
##
|
46
|
+
## Two examples for you to look at, though sadly neither of them is
|
47
|
+
## as simple as I'd like: mbox/loader.rb and imap.rb
|
48
|
+
|
49
|
+
|
42
50
|
|
43
51
|
## dirty? described whether cur_offset has changed, which means the
|
44
52
|
## source info needs to be re-saved to sources.yaml.
|
@@ -78,7 +86,7 @@ class Source
|
|
78
86
|
true
|
79
87
|
end
|
80
88
|
end
|
81
|
-
def is_source_for?
|
89
|
+
def is_source_for? uri; URI(self.uri) == URI(uri); end
|
82
90
|
|
83
91
|
def each
|
84
92
|
return if broken?
|
data/lib/sup/textfield.rb
CHANGED
@@ -29,6 +29,7 @@ class TextField
|
|
29
29
|
@w.mvaddstr @y, 0, @question
|
30
30
|
Ncurses.curs_set 1
|
31
31
|
Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD
|
32
|
+
Ncurses::Form.form_driver @form, Ncurses::Form::REQ_NEXT_CHAR if @history[@i] =~ / $/ # fucking RETARDED!!!!
|
32
33
|
end
|
33
34
|
|
34
35
|
def deactivate
|
data/lib/sup/thread.rb
CHANGED
@@ -1,4 +1,24 @@
|
|
1
|
-
|
1
|
+
## Herein is all the code responsible for threading messages. I use an
|
2
|
+
## online version of the JWZ threading algorithm:
|
3
|
+
## http://www.jwz.org/doc/threading.html
|
4
|
+
##
|
5
|
+
## I certainly didn't implement it for efficiency, but thanks to our
|
6
|
+
## search engine backend, it's typically not applied to very many
|
7
|
+
## messages at once.
|
8
|
+
|
9
|
+
## At the top level, we have a ThreadSet. A ThreadSet represents a set
|
10
|
+
## of threads, e.g. a message folder or an inbox. Each ThreadSet
|
11
|
+
## contains zero or more Threads. A Thread represents all the message
|
12
|
+
## related to a particular subject. Each Thread has one or more
|
13
|
+
## Containers. A Container is a recursive structure that holds the
|
14
|
+
## tree structure as determined by the references: and in-reply-to:
|
15
|
+
## headers. A Thread with multiple Containers occurs if they have the
|
16
|
+
## same subject, but (most likely due to someone using a primitive
|
17
|
+
## MUA) we don't have evidence from in-reply-to: or references:
|
18
|
+
## headers, only subject: (and thus our tree is probably broken). A
|
19
|
+
## Container holds zero or one message. In the case of no message, it
|
20
|
+
## means we've seen a reference to the message but haven't seen the
|
21
|
+
## message itself (yet).
|
2
22
|
|
3
23
|
module Redwood
|
4
24
|
|
@@ -30,9 +50,8 @@ class Thread
|
|
30
50
|
puts "=== end thread ==="
|
31
51
|
end
|
32
52
|
|
33
|
-
## yields each message, its depth, and its parent
|
34
|
-
##
|
35
|
-
## or nil.
|
53
|
+
## yields each message, its depth, and its parent. note that the
|
54
|
+
## message can be a Message object, or :fake_root, or nil.
|
36
55
|
def each fake_root=false
|
37
56
|
adj = 0
|
38
57
|
root = @containers.find_all { |c| !Message.subj_is_reply?(c) }.argmin { |c| c.date }
|
@@ -59,15 +78,14 @@ class Thread
|
|
59
78
|
end
|
60
79
|
end
|
61
80
|
|
81
|
+
def first; each { |m, *o| return m if m }; nil; end
|
62
82
|
def dirty?; any? { |m, *o| m && m.dirty? }; end
|
63
83
|
def date; map { |m, *o| m.date if m }.compact.max; end
|
64
84
|
def snippet; argfind { |m, *o| m && m.snippet }; end
|
65
85
|
def authors; map { |m, *o| m.from if m }.compact.uniq; end
|
66
86
|
|
67
87
|
def apply_label t; each { |m, *o| m && m.add_label(t) }; end
|
68
|
-
def remove_label t
|
69
|
-
each { |m, *o| m && m.remove_label(t) }
|
70
|
-
end
|
88
|
+
def remove_label t; each { |m, *o| m && m.remove_label(t) }; end
|
71
89
|
|
72
90
|
def toggle_label label
|
73
91
|
if has_label? label
|
@@ -82,7 +100,6 @@ class Thread
|
|
82
100
|
def set_labels l; each { |m, *o| m && m.labels = l }; end
|
83
101
|
|
84
102
|
def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
|
85
|
-
def dirty?; any? { |m, *o| m && m.dirty? }; end
|
86
103
|
def save index; each { |m, *o| m && m.save(index) }; end
|
87
104
|
|
88
105
|
def direct_participants
|
@@ -220,13 +237,13 @@ class ThreadSet
|
|
220
237
|
(c = @messages[m.id]) && c.root.thread
|
221
238
|
end
|
222
239
|
|
223
|
-
def
|
224
|
-
@subj_thread.each { |k, v| @subj_thread.delete(k) if v.empty? }
|
240
|
+
def delete_cruft
|
241
|
+
@subj_thread.each { |k, v| @subj_thread.delete(k) if v.empty? || v.subj != k }
|
225
242
|
end
|
226
|
-
private :
|
243
|
+
private :delete_cruft
|
227
244
|
|
228
|
-
def threads;
|
229
|
-
def size;
|
245
|
+
def threads; delete_cruft; @subj_thread.values; end
|
246
|
+
def size; delete_cruft; @subj_thread.size; end
|
230
247
|
|
231
248
|
def dump
|
232
249
|
@subj_thread.each do |s, t|
|
@@ -273,19 +290,25 @@ class ThreadSet
|
|
273
290
|
|
274
291
|
m = builder.call
|
275
292
|
add_message m
|
276
|
-
load_thread_for_message m
|
293
|
+
load_thread_for_message m, :load_killed => opts[:load_killed]
|
277
294
|
yield @subj_thread.size if block_given?
|
278
295
|
end
|
279
296
|
end
|
280
297
|
|
281
298
|
## loads in all messages needed to thread m
|
282
|
-
def load_thread_for_message m
|
283
|
-
@index.each_message_in_thread_for m, :limit => 100 do |mid, builder|
|
299
|
+
def load_thread_for_message m, opts={}
|
300
|
+
@index.each_message_in_thread_for m, opts.merge({:limit => 100}) do |mid, builder|
|
284
301
|
next if contains_id? mid
|
285
302
|
add_message builder.call
|
286
303
|
end
|
287
304
|
end
|
288
305
|
|
306
|
+
## merges in a pre-loaded thread
|
307
|
+
def add_thread t
|
308
|
+
raise "duplicate" if @subj_thread.values.member? t
|
309
|
+
t.each { |m, *o| add_message m }
|
310
|
+
end
|
311
|
+
|
289
312
|
def is_relevant? m
|
290
313
|
m.refs.any? { |ref_id| @messages[ref_id] }
|
291
314
|
end
|
@@ -323,38 +346,68 @@ class ThreadSet
|
|
323
346
|
|
324
347
|
if root == oldroot
|
325
348
|
if oldroot.thread
|
326
|
-
|
349
|
+
## check to see if the subject is still the same (in the case
|
350
|
+
## that we first added a child message with a different
|
351
|
+
## subject)
|
352
|
+
|
353
|
+
## this code is duplicated below. sorry! TODO: refactor
|
354
|
+
s = Message.normalize_subj(root.subj)
|
355
|
+
unless @subj_thread[s] == root.thread
|
356
|
+
## Redwood::log "[1] moving thread to new subject #{root.subj}"
|
357
|
+
if @subj_thread[s]
|
358
|
+
@subj_thread[s] << root
|
359
|
+
root.thread = @subj_thread[s]
|
360
|
+
else
|
361
|
+
@subj_thread[s] = root.thread
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
327
365
|
else
|
328
366
|
## to disable subject grouping, use the next line instead
|
329
367
|
## (and the same for below)
|
330
|
-
#Redwood::log "[1]
|
368
|
+
#Redwood::log "[1] for #{root}, subject #{Message.normalize_subj(root.subj)} has #{@subj_thread[Message.normalize_subj(root.subj)] ? 'a' : 'no'} thread"
|
331
369
|
thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
|
332
370
|
#thread = (@subj_thread[root.id] ||= Thread.new)
|
333
371
|
|
334
372
|
thread << root
|
335
373
|
root.thread = thread
|
336
|
-
#
|
374
|
+
# Redwood::log "[1] added #{root} to #{thread}"
|
337
375
|
end
|
338
376
|
else
|
339
377
|
if oldroot.thread
|
340
378
|
## new root. need to drop old one and put this one in its place
|
341
|
-
# puts "*** DROPPING #{oldroot} from #{oldroot.thread}"
|
342
379
|
oldroot.thread.drop oldroot
|
343
380
|
oldroot.thread = nil
|
344
381
|
end
|
345
382
|
|
346
383
|
if root.thread
|
347
|
-
|
384
|
+
## check to see if the subject is still the same (in the case
|
385
|
+
## that we first added a child message with a different
|
386
|
+
## subject)
|
387
|
+
s = Message.normalize_subj(root.subj)
|
388
|
+
unless @subj_thread[s] == root.thread
|
389
|
+
# Redwood::log "[2] moving thread to new subject #{root.subj}"
|
390
|
+
if @subj_thread[s]
|
391
|
+
@subj_thread[s] << root
|
392
|
+
root.thread = @subj_thread[s]
|
393
|
+
else
|
394
|
+
@subj_thread[s] = root.thread
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
348
398
|
else
|
349
399
|
## to disable subject grouping, use the next line instead
|
350
400
|
## (and the same above)
|
351
|
-
|
401
|
+
|
402
|
+
## this code is duplicated above. sorry! TODO: refactor
|
403
|
+
# Redwood::log "[2] for #{root}, subject '#{Message.normalize_subj(root.subj)}' has #{@subj_thread[Message.normalize_subj(root.subj)] ? 'a' : 'no'} thread"
|
404
|
+
|
352
405
|
thread = (@subj_thread[Message.normalize_subj(root.subj)] ||= Thread.new)
|
353
406
|
#thread = (@subj_thread[root.id] ||= Thread.new)
|
354
407
|
|
355
408
|
thread << root
|
356
409
|
root.thread = thread
|
357
|
-
#
|
410
|
+
# Redwood::log "[2] added #{root} to #{thread}"
|
358
411
|
end
|
359
412
|
end
|
360
413
|
|
data/lib/sup/update.rb
CHANGED
@@ -11,10 +11,9 @@ class UpdateManager
|
|
11
11
|
def register o; @targets[o] = true; end
|
12
12
|
def unregister o; @targets.delete o; end
|
13
13
|
|
14
|
-
def relay type, *args
|
14
|
+
def relay sender, type, *args
|
15
15
|
meth = "handle_#{type}_update".intern
|
16
|
-
@targets.keys.each { |o| o.send meth, *args if o.respond_to? meth }
|
17
|
-
BufferManager.draw_screen ## TODO: think about this
|
16
|
+
@targets.keys.each { |o| o.send meth, sender, *args unless o == sender if o.respond_to? meth }
|
18
17
|
end
|
19
18
|
end
|
20
19
|
|
data/lib/sup/util.rb
CHANGED
@@ -43,6 +43,19 @@ class Object
|
|
43
43
|
## i'm sure there's pithy comment i could make here about the
|
44
44
|
## superiority of lisp, but fuck lisp.
|
45
45
|
def returning x; yield x; x; end
|
46
|
+
|
47
|
+
## clone of java-style whole-method synchronization
|
48
|
+
## assumes a @mutex variable
|
49
|
+
def synchronized *meth
|
50
|
+
meth.each do
|
51
|
+
class_eval <<-EOF
|
52
|
+
alias unsynchronized_#{meth} #{meth}
|
53
|
+
def #{meth}(*a, &b)
|
54
|
+
@mutex.synchronize { unsynchronized_#{meth}(*a, &b) }
|
55
|
+
end
|
56
|
+
EOF
|
57
|
+
end
|
58
|
+
end
|
46
59
|
end
|
47
60
|
|
48
61
|
class String
|
@@ -176,19 +189,6 @@ class Array
|
|
176
189
|
def rest; self[1..-1]; end
|
177
190
|
|
178
191
|
def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
|
179
|
-
|
180
|
-
## apparently uniq doesn't use ==. wtf.
|
181
|
-
def remove_successive_dupes
|
182
|
-
ret = []
|
183
|
-
last = nil
|
184
|
-
each do |e|
|
185
|
-
unless e == last
|
186
|
-
ret << e
|
187
|
-
last = e
|
188
|
-
end
|
189
|
-
end
|
190
|
-
ret
|
191
|
-
end
|
192
192
|
end
|
193
193
|
|
194
194
|
class Time
|