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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTORS +84 -0
- data/Gemfile +3 -0
- data/HACKING +42 -0
- data/History.txt +361 -0
- data/LICENSE +280 -0
- data/README.md +70 -0
- data/Rakefile +12 -0
- data/ReleaseNotes +231 -0
- data/bin/sup +434 -0
- data/bin/sup-add +118 -0
- data/bin/sup-config +243 -0
- data/bin/sup-dump +43 -0
- data/bin/sup-import-dump +101 -0
- data/bin/sup-psych-ify-config-files +21 -0
- data/bin/sup-recover-sources +87 -0
- data/bin/sup-sync +210 -0
- data/bin/sup-sync-back-maildir +127 -0
- data/bin/sup-tweak-labels +140 -0
- data/contrib/colorpicker.rb +100 -0
- data/contrib/completion/_sup.zsh +114 -0
- data/devel/console.sh +3 -0
- data/devel/count-loc.sh +3 -0
- data/devel/load-index.rb +9 -0
- data/devel/profile.rb +12 -0
- data/devel/start-console.rb +5 -0
- data/doc/FAQ.txt +119 -0
- data/doc/Hooks.txt +79 -0
- data/doc/Philosophy.txt +69 -0
- data/lib/sup.rb +467 -0
- data/lib/sup/account.rb +90 -0
- data/lib/sup/buffer.rb +768 -0
- data/lib/sup/colormap.rb +239 -0
- data/lib/sup/contact.rb +67 -0
- data/lib/sup/crypto.rb +461 -0
- data/lib/sup/draft.rb +119 -0
- data/lib/sup/hook.rb +159 -0
- data/lib/sup/horizontal_selector.rb +59 -0
- data/lib/sup/idle.rb +42 -0
- data/lib/sup/index.rb +882 -0
- data/lib/sup/interactive_lock.rb +89 -0
- data/lib/sup/keymap.rb +140 -0
- data/lib/sup/label.rb +87 -0
- data/lib/sup/logger.rb +77 -0
- data/lib/sup/logger/singleton.rb +10 -0
- data/lib/sup/maildir.rb +257 -0
- data/lib/sup/mbox.rb +187 -0
- data/lib/sup/message.rb +803 -0
- data/lib/sup/message_chunks.rb +328 -0
- data/lib/sup/mode.rb +140 -0
- data/lib/sup/modes/buffer_list_mode.rb +50 -0
- data/lib/sup/modes/completion_mode.rb +55 -0
- data/lib/sup/modes/compose_mode.rb +38 -0
- data/lib/sup/modes/console_mode.rb +125 -0
- data/lib/sup/modes/contact_list_mode.rb +148 -0
- data/lib/sup/modes/edit_message_async_mode.rb +110 -0
- data/lib/sup/modes/edit_message_mode.rb +728 -0
- data/lib/sup/modes/file_browser_mode.rb +109 -0
- data/lib/sup/modes/forward_mode.rb +82 -0
- data/lib/sup/modes/help_mode.rb +19 -0
- data/lib/sup/modes/inbox_mode.rb +85 -0
- data/lib/sup/modes/label_list_mode.rb +138 -0
- data/lib/sup/modes/label_search_results_mode.rb +38 -0
- data/lib/sup/modes/line_cursor_mode.rb +203 -0
- data/lib/sup/modes/log_mode.rb +57 -0
- data/lib/sup/modes/person_search_results_mode.rb +12 -0
- data/lib/sup/modes/poll_mode.rb +19 -0
- data/lib/sup/modes/reply_mode.rb +228 -0
- data/lib/sup/modes/resume_mode.rb +52 -0
- data/lib/sup/modes/scroll_mode.rb +252 -0
- data/lib/sup/modes/search_list_mode.rb +204 -0
- data/lib/sup/modes/search_results_mode.rb +59 -0
- data/lib/sup/modes/text_mode.rb +76 -0
- data/lib/sup/modes/thread_index_mode.rb +1033 -0
- data/lib/sup/modes/thread_view_mode.rb +941 -0
- data/lib/sup/person.rb +134 -0
- data/lib/sup/poll.rb +272 -0
- data/lib/sup/rfc2047.rb +56 -0
- data/lib/sup/search.rb +110 -0
- data/lib/sup/sent.rb +58 -0
- data/lib/sup/service/label_service.rb +45 -0
- data/lib/sup/source.rb +244 -0
- data/lib/sup/tagger.rb +50 -0
- data/lib/sup/textfield.rb +253 -0
- data/lib/sup/thread.rb +452 -0
- data/lib/sup/time.rb +93 -0
- data/lib/sup/undo.rb +38 -0
- data/lib/sup/update.rb +30 -0
- data/lib/sup/util.rb +747 -0
- data/lib/sup/util/ncurses.rb +274 -0
- data/lib/sup/util/path.rb +9 -0
- data/lib/sup/util/query.rb +17 -0
- data/lib/sup/util/uri.rb +15 -0
- data/lib/sup/version.rb +3 -0
- data/sup.gemspec +53 -0
- data/test/dummy_source.rb +61 -0
- data/test/gnupg_test_home/gpg.conf +1 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
- data/test/gnupg_test_home/trustdb.gpg +0 -0
- data/test/integration/test_label_service.rb +18 -0
- data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
- data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
- data/test/messages/missing-line.eml +9 -0
- data/test/test_crypto.rb +109 -0
- data/test/test_header_parsing.rb +168 -0
- data/test/test_helper.rb +7 -0
- data/test/test_message.rb +532 -0
- data/test/test_messages_dir.rb +147 -0
- data/test/test_yaml_migration.rb +85 -0
- data/test/test_yaml_regressions.rb +17 -0
- data/test/unit/service/test_label_service.rb +19 -0
- data/test/unit/test_horizontal_selector.rb +40 -0
- data/test/unit/util/test_query.rb +46 -0
- data/test/unit/util/test_string.rb +57 -0
- data/test/unit/util/test_uri.rb +19 -0
- metadata +423 -0
data/lib/sup/time.rb
ADDED
|
@@ -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
|
+
|
data/lib/sup/undo.rb
ADDED
|
@@ -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
|
data/lib/sup/update.rb
ADDED
|
@@ -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
|
data/lib/sup/util.rb
ADDED
|
@@ -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
|
+
|