sup 0.0.8 → 0.1
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 +6 -36
- data/History.txt +11 -0
- data/Manifest.txt +5 -0
- data/README.txt +13 -31
- data/Rakefile +3 -3
- data/bin/sup +167 -89
- data/bin/sup-add +39 -29
- data/bin/sup-config +57 -31
- data/bin/sup-sync +60 -54
- data/bin/sup-sync-back +143 -0
- data/doc/FAQ.txt +56 -19
- data/doc/Philosophy.txt +34 -33
- data/doc/TODO +76 -46
- data/doc/UserGuide.txt +142 -122
- data/lib/sup.rb +76 -36
- data/lib/sup/account.rb +27 -19
- data/lib/sup/buffer.rb +130 -44
- data/lib/sup/contact.rb +1 -1
- data/lib/sup/draft.rb +1 -2
- data/lib/sup/imap.rb +64 -19
- data/lib/sup/index.rb +95 -16
- data/lib/sup/keymap.rb +1 -1
- data/lib/sup/label.rb +31 -5
- data/lib/sup/maildir.rb +7 -5
- data/lib/sup/mbox.rb +34 -15
- data/lib/sup/mbox/loader.rb +30 -12
- data/lib/sup/mbox/ssh-loader.rb +7 -5
- data/lib/sup/message.rb +93 -44
- data/lib/sup/modes/buffer-list-mode.rb +1 -1
- data/lib/sup/modes/completion-mode.rb +55 -0
- data/lib/sup/modes/compose-mode.rb +6 -25
- data/lib/sup/modes/contact-list-mode.rb +1 -1
- data/lib/sup/modes/edit-message-mode.rb +119 -29
- data/lib/sup/modes/file-browser-mode.rb +108 -0
- data/lib/sup/modes/forward-mode.rb +3 -20
- data/lib/sup/modes/inbox-mode.rb +9 -12
- data/lib/sup/modes/label-list-mode.rb +28 -46
- data/lib/sup/modes/label-search-results-mode.rb +1 -16
- data/lib/sup/modes/line-cursor-mode.rb +44 -5
- data/lib/sup/modes/person-search-results-mode.rb +1 -16
- data/lib/sup/modes/reply-mode.rb +18 -31
- data/lib/sup/modes/resume-mode.rb +6 -6
- data/lib/sup/modes/scroll-mode.rb +6 -5
- data/lib/sup/modes/search-results-mode.rb +6 -17
- data/lib/sup/modes/thread-index-mode.rb +70 -28
- data/lib/sup/modes/thread-view-mode.rb +65 -29
- data/lib/sup/person.rb +71 -30
- data/lib/sup/poll.rb +13 -4
- data/lib/sup/rfc2047.rb +61 -0
- data/lib/sup/sent.rb +7 -5
- data/lib/sup/source.rb +12 -9
- data/lib/sup/suicide.rb +36 -0
- data/lib/sup/tagger.rb +6 -6
- data/lib/sup/textfield.rb +76 -14
- data/lib/sup/thread.rb +97 -123
- data/lib/sup/util.rb +167 -1
- metadata +30 -5
data/lib/sup/contact.rb
CHANGED
@@ -12,7 +12,7 @@ class ContactManager
|
|
12
12
|
IO.foreach(fn) do |l|
|
13
13
|
l =~ /^(\S+): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
|
14
14
|
aalias, addr = $1, $2
|
15
|
-
p =
|
15
|
+
p = PersonManager.person_for addr, :definitive => true
|
16
16
|
@p2a[p] = aalias
|
17
17
|
@a2p[aalias] = p
|
18
18
|
end
|
data/lib/sup/draft.rb
CHANGED
@@ -42,6 +42,7 @@ end
|
|
42
42
|
|
43
43
|
class DraftLoader < Source
|
44
44
|
attr_accessor :dir
|
45
|
+
yaml_properties :cur_offset
|
45
46
|
|
46
47
|
def initialize cur_offset=0
|
47
48
|
dir = Redwood::DRAFT_DIR
|
@@ -119,6 +120,4 @@ private
|
|
119
120
|
end
|
120
121
|
end
|
121
122
|
|
122
|
-
Redwood::register_yaml(DraftLoader, %w(cur_offset))
|
123
|
-
|
124
123
|
end
|
data/lib/sup/imap.rb
CHANGED
@@ -6,7 +6,7 @@ require 'rmail'
|
|
6
6
|
|
7
7
|
## fucking imap fucking sucks. what the FUCK kind of committee of
|
8
8
|
## dunces designed this shit.
|
9
|
-
|
9
|
+
##
|
10
10
|
## imap talks about 'unique ids' for messages, to be used for
|
11
11
|
## cross-session identification. great---just what sup needs! except
|
12
12
|
## it turns out the uids can be invalidated every time the
|
@@ -19,13 +19,25 @@ require 'rmail'
|
|
19
19
|
## does. thus the so-called uids are absolutely useless and imap
|
20
20
|
## provides no cross-session way of uniquely identifying a
|
21
21
|
## message. but thanks for the "strong recommendation", guys!
|
22
|
-
|
22
|
+
##
|
23
23
|
## so right now i'm using the 'internal date' and the size of each
|
24
24
|
## message to uniquely identify it, and i scan over the entire mailbox
|
25
25
|
## each time i open it to map those things to message ids. that can be
|
26
26
|
## slow for large mailboxes, and we'll just have to hope that there
|
27
27
|
## are no collisions. ho ho! a perfectly reasonable solution!
|
28
|
-
|
28
|
+
##
|
29
|
+
## and here's another thing. check out RFC2060 2.2.2 paragraph 5:
|
30
|
+
##
|
31
|
+
## A client MUST be prepared to accept any server response at all times.
|
32
|
+
## This includes server data that was not requested.
|
33
|
+
##
|
34
|
+
## yeah. that totally makes a lot of sense. and once again, the idiocy
|
35
|
+
## of the spec actually happens in practice. you'll request flags for
|
36
|
+
## one message, and get it interspersed with a random bunch of flags
|
37
|
+
## for some other messages, including a different set of flags for the
|
38
|
+
## same message! totally ok by the imap spec. totally retarded by any
|
39
|
+
## other metric.
|
40
|
+
##
|
29
41
|
## fuck you, imap committee. you managed to design something nearly as
|
30
42
|
## shitty as mbox but goddamn THIRTY YEARS LATER.
|
31
43
|
module Redwood
|
@@ -34,11 +46,13 @@ class IMAP < Source
|
|
34
46
|
SCAN_INTERVAL = 60 # seconds
|
35
47
|
|
36
48
|
## upon these errors we'll try to rereconnect a few times
|
37
|
-
RECOVERABLE_ERRORS = [ Errno::EPIPE, Errno::ETIMEDOUT ]
|
49
|
+
RECOVERABLE_ERRORS = [ Errno::EPIPE, Errno::ETIMEDOUT, OpenSSL::SSL::SSLError ]
|
38
50
|
|
39
51
|
attr_accessor :username, :password
|
52
|
+
yaml_properties :uri, :username, :password, :cur_offset, :usual,
|
53
|
+
:archived, :id, :labels
|
40
54
|
|
41
|
-
def initialize uri, username, password, last_idate=nil, usual=true, archived=false, id=nil
|
55
|
+
def initialize uri, username, password, last_idate=nil, usual=true, archived=false, id=nil, labels=[]
|
42
56
|
raise ArgumentError, "username and password must be specified" unless username && password
|
43
57
|
raise ArgumentError, "not an imap uri" unless uri =~ %r!imaps?://!
|
44
58
|
|
@@ -51,11 +65,19 @@ class IMAP < Source
|
|
51
65
|
@imap_ids = {}
|
52
66
|
@ids = []
|
53
67
|
@last_scan = nil
|
54
|
-
@labels = [
|
55
|
-
@
|
68
|
+
@labels = (labels || []).freeze
|
69
|
+
@say_id = nil
|
56
70
|
@mutex = Mutex.new
|
57
71
|
end
|
58
72
|
|
73
|
+
def self.suggest_labels_for path
|
74
|
+
if path =~ /inbox/i
|
75
|
+
[path.intern]
|
76
|
+
else
|
77
|
+
[]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
59
81
|
def host; @parsed_uri.host; end
|
60
82
|
def port; @parsed_uri.port || (ssl? ? 993 : 143); end
|
61
83
|
def mailbox
|
@@ -88,6 +110,7 @@ class IMAP < Source
|
|
88
110
|
def raw_header id
|
89
111
|
unsynchronized_scan_mailbox
|
90
112
|
header, flags = get_imap_fields id, 'RFC822.HEADER', 'FLAGS'
|
113
|
+
## very bad. this is very very bad. very bad bad bad.
|
91
114
|
header = header + "Status: RO\n" if flags.include? :Seen # fake an mbox-style read header # TODO: improve source-marked-as-read reporting system
|
92
115
|
header.gsub(/\r\n/, "\n")
|
93
116
|
end
|
@@ -115,9 +138,9 @@ class IMAP < Source
|
|
115
138
|
|
116
139
|
return if last_id == @ids.length
|
117
140
|
|
118
|
-
|
119
|
-
|
120
|
-
|
141
|
+
range = (@ids.length + 1) .. last_id
|
142
|
+
Redwood::log "fetching IMAP headers #{range}"
|
143
|
+
fetch(range, ['RFC822.SIZE', 'INTERNALDATE']).each do |v|
|
121
144
|
id = make_id v
|
122
145
|
@ids << id
|
123
146
|
@imap_ids[id] = v.seqno
|
@@ -137,7 +160,7 @@ class IMAP < Source
|
|
137
160
|
start.upto(ids.length - 1) do |i|
|
138
161
|
id = ids[i]
|
139
162
|
self.cur_offset = id
|
140
|
-
yield id, @labels
|
163
|
+
yield id, @labels
|
141
164
|
end
|
142
165
|
end
|
143
166
|
|
@@ -157,6 +180,24 @@ class IMAP < Source
|
|
157
180
|
|
158
181
|
private
|
159
182
|
|
183
|
+
def fetch ids, fields
|
184
|
+
results = safely { @imap.fetch ids, fields }
|
185
|
+
good_results =
|
186
|
+
if ids.respond_to? :member?
|
187
|
+
results.find_all { |r| ids.member?(r.seqno) && fields.all? { |f| r.attr.member?(f) } }
|
188
|
+
else
|
189
|
+
results.find_all { |r| ids == r.seqno && fields.all? { |f| r.attr.member?(f) } }
|
190
|
+
end
|
191
|
+
|
192
|
+
if good_results.empty?
|
193
|
+
raise FatalSourceError, "no IMAP response for #{ids} containing all fields #{fields.join(', ')} (got #{results.size} results)"
|
194
|
+
elsif good_results.size < results.size
|
195
|
+
Redwood::log "Your IMAP server sucks. It sent #{results.size} results for a request for #{good_results.size} messages. What are you using, Binc?"
|
196
|
+
end
|
197
|
+
|
198
|
+
good_results
|
199
|
+
end
|
200
|
+
|
160
201
|
def unsafe_connect
|
161
202
|
say "Connecting to IMAP server #{host}:#{port}..."
|
162
203
|
|
@@ -209,6 +250,10 @@ private
|
|
209
250
|
|
210
251
|
def make_id imap_stuff
|
211
252
|
# use 7 digits for the size. why 7? seems nice.
|
253
|
+
%w(RFC822.SIZE INTERNALDATE).each do |w|
|
254
|
+
raise FatalSourceError, "requested data not in IMAP response: #{w}" unless imap_stuff.attr[w]
|
255
|
+
end
|
256
|
+
|
212
257
|
msize, mdate = imap_stuff.attr['RFC822.SIZE'] % 10000000, Time.parse(imap_stuff.attr["INTERNALDATE"])
|
213
258
|
sprintf("%d%07d", mdate.to_i, msize).to_i
|
214
259
|
end
|
@@ -217,11 +262,11 @@ private
|
|
217
262
|
imap_id = @imap_ids[id] or raise OutOfSyncSourceError, "Unknown message id #{id}"
|
218
263
|
|
219
264
|
retried = false
|
220
|
-
|
221
|
-
got_id = make_id
|
265
|
+
result = fetch(imap_id, (fields + ['RFC822.SIZE', 'INTERNALDATE']).uniq).first
|
266
|
+
got_id = make_id result
|
222
267
|
raise OutOfSyncSourceError, "IMAP message mismatch: requested #{id}, got #{got_id}." unless got_id == id
|
223
268
|
|
224
|
-
fields.map { |f|
|
269
|
+
fields.map { |f| result.attr[f] or raise FatalSourceError, "empty response from IMAP server: #{f}" }
|
225
270
|
end
|
226
271
|
|
227
272
|
## execute a block, connected if unconnected, re-connected up to 3
|
@@ -233,20 +278,20 @@ private
|
|
233
278
|
begin
|
234
279
|
unsafe_connect unless @imap
|
235
280
|
yield
|
236
|
-
rescue *RECOVERABLE_ERRORS
|
281
|
+
rescue *RECOVERABLE_ERRORS => e
|
237
282
|
if (retries += 1) <= 3
|
238
283
|
@imap = nil
|
284
|
+
Redwood::log "got #{e.class.name}: #{e.message.inspect}"
|
285
|
+
sleep 2
|
239
286
|
retry
|
240
287
|
end
|
241
288
|
raise
|
242
289
|
end
|
243
|
-
rescue SocketError, Net::IMAP::Error, SystemCallError, IOError => e
|
244
|
-
raise FatalSourceError, "While communicating with IMAP server: #{e.message}"
|
290
|
+
rescue SocketError, Net::IMAP::Error, SystemCallError, IOError, OpenSSL::SSL::SSLError => e
|
291
|
+
raise FatalSourceError, "While communicating with IMAP server (type #{e.class.name}): #{e.message.inspect}"
|
245
292
|
end
|
246
293
|
end
|
247
294
|
|
248
295
|
end
|
249
296
|
|
250
|
-
Redwood::register_yaml(IMAP, %w(uri username password cur_offset usual archived id))
|
251
|
-
|
252
297
|
end
|
data/lib/sup/index.rb
CHANGED
@@ -7,6 +7,14 @@ require 'ferret'
|
|
7
7
|
module Redwood
|
8
8
|
|
9
9
|
class Index
|
10
|
+
class LockError < StandardError
|
11
|
+
def initialize h
|
12
|
+
@h = h
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing m; @h[m.to_s] end
|
16
|
+
end
|
17
|
+
|
10
18
|
include Singleton
|
11
19
|
|
12
20
|
attr_reader :index
|
@@ -16,14 +24,79 @@ class Index
|
|
16
24
|
@sources_dirty = false
|
17
25
|
|
18
26
|
wsa = Ferret::Analysis::WhiteSpaceAnalyzer.new false
|
19
|
-
sa = Ferret::Analysis::StandardAnalyzer.new
|
27
|
+
sa = Ferret::Analysis::StandardAnalyzer.new [], true
|
20
28
|
@analyzer = Ferret::Analysis::PerFieldAnalyzer.new wsa
|
21
29
|
@analyzer[:body] = sa
|
22
|
-
@
|
30
|
+
@analyzer[:subject] = sa
|
31
|
+
@qparser ||= Ferret::QueryParser.new :default_field => :body, :analyzer => @analyzer, :or_default => false
|
32
|
+
@lock = Lockfile.new lockfile, :retries => 0, :max_age => nil
|
23
33
|
|
24
34
|
self.class.i_am_the_instance self
|
25
35
|
end
|
26
36
|
|
37
|
+
def lockfile; File.join @dir, "lock" end
|
38
|
+
|
39
|
+
def lock
|
40
|
+
Redwood::log "locking #{lockfile}..."
|
41
|
+
begin
|
42
|
+
@lock.lock
|
43
|
+
rescue Lockfile::MaxTriesLockError
|
44
|
+
raise LockError, @lock.lockinfo_on_disk
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def start_lock_update_thread
|
49
|
+
@lock_update_thread = Redwood::reporting_thread do
|
50
|
+
while true
|
51
|
+
sleep 30
|
52
|
+
@lock.touch_yourself
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def stop_lock_update_thread
|
58
|
+
@lock_update_thread.kill if @lock_update_thread
|
59
|
+
@lock_update_thread = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def fancy_lock_error_message_for e
|
63
|
+
secs = Time.now - e.mtime
|
64
|
+
mins = secs.to_i / 60
|
65
|
+
time =
|
66
|
+
if mins == 0
|
67
|
+
"#{secs.to_i} seconds"
|
68
|
+
else
|
69
|
+
"#{mins} minutes"
|
70
|
+
end
|
71
|
+
|
72
|
+
<<EOS
|
73
|
+
Error: the sup index is locked by another process! User '#{e.user}' on
|
74
|
+
host '#{e.host}' is running #{e.pname} with pid #{e.pid}. The process was alive
|
75
|
+
as of #{time} ago.
|
76
|
+
EOS
|
77
|
+
end
|
78
|
+
|
79
|
+
def lock_or_die
|
80
|
+
begin
|
81
|
+
lock
|
82
|
+
rescue LockError => e
|
83
|
+
$stderr.puts fancy_lock_error_message_for(e)
|
84
|
+
$stderr.puts <<EOS
|
85
|
+
|
86
|
+
You can wait for the process to finish, or, if it crashed and left a
|
87
|
+
stale lock file behind, you can manually delete #{@lock.path}.
|
88
|
+
EOS
|
89
|
+
exit
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def unlock
|
94
|
+
if @lock && @lock.locked?
|
95
|
+
Redwood::log "unlocking #{lockfile}..."
|
96
|
+
@lock.unlock
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
27
100
|
def load
|
28
101
|
load_sources
|
29
102
|
load_index
|
@@ -116,7 +189,7 @@ class Index
|
|
116
189
|
end
|
117
190
|
|
118
191
|
def save_index fn=File.join(@dir, "ferret")
|
119
|
-
# don't have to do anything,
|
192
|
+
# don't have to do anything, apparently
|
120
193
|
end
|
121
194
|
|
122
195
|
def contains_id? id
|
@@ -143,25 +216,25 @@ class Index
|
|
143
216
|
|
144
217
|
def num_results_for opts={}
|
145
218
|
return 0 if @index.size == 0 # otherwise ferret barfs ###TODO: remove this once my ferret patch is accepted
|
219
|
+
|
146
220
|
q = build_query opts
|
147
|
-
index.search(q).total_hits
|
221
|
+
index.search(q, :limit => 1).total_hits
|
148
222
|
end
|
149
223
|
|
150
224
|
## yield all messages in the thread containing 'm' by repeatedly
|
151
|
-
## querying the index.
|
225
|
+
## querying the index. yields pairs of message ids and
|
152
226
|
## message-building lambdas, so that building an unwanted message
|
153
227
|
## can be skipped in the block if desired.
|
154
228
|
##
|
155
229
|
## stops loading any thread if a message with a :killed flag is found.
|
156
|
-
|
157
230
|
SAME_SUBJECT_DATE_LIMIT = 7
|
158
231
|
def each_message_in_thread_for m, opts={}
|
232
|
+
Redwood::log "Building thread for #{m.id}: #{m.subj}"
|
159
233
|
messages = {}
|
160
234
|
searched = {}
|
161
235
|
num_queries = 0
|
162
236
|
|
163
|
-
|
164
|
-
if true # do subject queries
|
237
|
+
if $config[:thread_by_subject] # do subject queries
|
165
238
|
date_min = m.date - (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
|
166
239
|
date_max = m.date + (SAME_SUBJECT_DATE_LIMIT * 12 * 3600)
|
167
240
|
|
@@ -196,14 +269,15 @@ class Index
|
|
196
269
|
break if opts[:limit] && messages.size >= opts[:limit]
|
197
270
|
break if @index[docid][:label].split(/\s+/).include? "killed" unless opts[:load_killed]
|
198
271
|
mid = @index[docid][:message_id]
|
199
|
-
unless messages.member?
|
272
|
+
unless messages.member?(mid)
|
273
|
+
Redwood::log "got #{mid} as a child of #{id}"
|
200
274
|
messages[mid] ||= lambda { build_message docid }
|
201
275
|
refs = @index[docid][:refs].split(" ")
|
202
276
|
pending += refs
|
203
277
|
end
|
204
278
|
end
|
205
279
|
end
|
206
|
-
Redwood::log "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}" if num_queries > 0
|
280
|
+
Redwood::log "ran #{num_queries} queries to build thread of #{messages.size + 1} messages for #{m.id}" if num_queries > 0
|
207
281
|
messages.each { |mid, builder| yield mid, builder }
|
208
282
|
end
|
209
283
|
|
@@ -261,11 +335,9 @@ class Index
|
|
261
335
|
t = @index[docid][:to]
|
262
336
|
|
263
337
|
if AccountManager.is_account_email? f
|
264
|
-
t.split(" ").each { |e|
|
265
|
-
contacts[Person.for(e)] = true }
|
338
|
+
t.split(" ").each { |e| contacts[PersonManager.person_for(e)] = true }
|
266
339
|
else
|
267
|
-
|
268
|
-
contacts[Person.for(f)] = true
|
340
|
+
contacts[PersonManager.person_for(f)] = true
|
269
341
|
end
|
270
342
|
end
|
271
343
|
|
@@ -278,6 +350,13 @@ class Index
|
|
278
350
|
@sources_dirty = false
|
279
351
|
end
|
280
352
|
|
353
|
+
def has_any_from_source_with_label? source, label
|
354
|
+
q = Ferret::Search::BooleanQuery.new
|
355
|
+
q.add_query Ferret::Search::TermQuery.new("source_id", source.id.to_s), :must
|
356
|
+
q.add_query Ferret::Search::TermQuery.new("label", label.to_s), :must
|
357
|
+
index.search(q, :limit => 1).total_hits > 0
|
358
|
+
end
|
359
|
+
|
281
360
|
protected
|
282
361
|
|
283
362
|
def parse_user_query_string str; @qparser.parse str; end
|
@@ -306,9 +385,9 @@ protected
|
|
306
385
|
bakfn = fn + ".bak"
|
307
386
|
if File.exists? fn
|
308
387
|
File.chmod 0600, fn
|
309
|
-
FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(
|
388
|
+
FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
|
310
389
|
end
|
311
|
-
Redwood::save_yaml_obj @sources.values, fn
|
390
|
+
Redwood::save_yaml_obj @sources.values.sort_by { |s| s.id.to_i }, fn, true
|
312
391
|
File.chmod 0600, fn
|
313
392
|
end
|
314
393
|
@sources_dirty = false
|
data/lib/sup/keymap.rb
CHANGED
@@ -81,7 +81,7 @@ class Keymap
|
|
81
81
|
next if valid_keys.empty?
|
82
82
|
[valid_keys.map { |k| keysym_to_string k }.join(", "), help]
|
83
83
|
end.compact
|
84
|
-
llen = lines.
|
84
|
+
llen = lines.max_of { |a, b| a.length }
|
85
85
|
lines.map { |a, b| sprintf " %#{llen}s : %s", a, b }.join("\n")
|
86
86
|
end
|
87
87
|
end
|
data/lib/sup/label.rb
CHANGED
@@ -8,10 +8,10 @@ class LabelManager
|
|
8
8
|
RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted ]
|
9
9
|
|
10
10
|
## labels which it nonetheless makes sense to search for by
|
11
|
-
|
11
|
+
LISTABLE_RESERVED_LABELS = [ :starred, :spam, :draft, :sent, :killed, :deleted ]
|
12
12
|
|
13
13
|
## labels that will never be displayed to the user
|
14
|
-
|
14
|
+
HIDDEN_RESERVED_LABELS = [ :starred, :unread ]
|
15
15
|
|
16
16
|
def initialize fn
|
17
17
|
@fn = fn
|
@@ -22,15 +22,41 @@ class LabelManager
|
|
22
22
|
[]
|
23
23
|
end
|
24
24
|
@labels = {}
|
25
|
+
@modified = false
|
25
26
|
labels.each { |t| @labels[t] = true }
|
26
27
|
|
27
28
|
self.class.i_am_the_instance self
|
28
29
|
end
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
31
|
+
## all listable (user-defined and system listable) labels, ordered
|
32
|
+
## nicely and converted to pretty strings. use #label_for to recover
|
33
|
+
## the original label.
|
34
|
+
def listable_label_strings
|
35
|
+
LISTABLE_RESERVED_LABELS.sort_by { |l| l.to_s }.map { |l| l.to_s.ucfirst } +
|
36
|
+
@labels.keys.map { |l| l.to_s }.sort
|
37
|
+
end
|
38
|
+
|
39
|
+
## reverse the label->string mapping, for convenience!
|
40
|
+
def label_for string
|
41
|
+
string.downcase.intern
|
42
|
+
end
|
43
|
+
|
44
|
+
def << t
|
45
|
+
t = t.intern unless t.is_a? Symbol
|
46
|
+
unless @labels.member?(t) || RESERVED_LABELS.member?(t)
|
47
|
+
@labels[t] = true
|
48
|
+
@modified = true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete t
|
53
|
+
if @labels.delete t
|
54
|
+
@modified = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
33
58
|
def save
|
59
|
+
return unless @modified
|
34
60
|
File.open(@fn, "w") { |f| f.puts @labels.keys }
|
35
61
|
end
|
36
62
|
end
|