]> git.cworth.org Git - sup/blob - lib/sup.rb
rewrite Singleton to not require i_am_the_instance
[sup] / lib / sup.rb
1 require 'rubygems'
2 require 'yaml'
3 require 'zlib'
4 require 'thread'
5 require 'fileutils'
6 require 'gettext'
7 require 'curses'
8 begin
9   require 'fastthread'
10 rescue LoadError
11 end
12
13 class Object
14   ## this is for debugging purposes because i keep calling #id on the
15   ## wrong object and i want it to throw an exception
16   def id
17     raise "wrong id called on #{self.inspect}"
18   end
19 end
20
21 class Module
22   def yaml_properties *props
23     props = props.map { |p| p.to_s }
24     vars = props.map { |p| "@#{p}" }
25     klass = self
26     path = klass.name.gsub(/::/, "/")
27
28     klass.instance_eval do
29       define_method(:to_yaml_properties) { vars }
30       define_method(:to_yaml_type) { "!#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}" }
31     end
32
33     YAML.add_domain_type("#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}", path) do |type, val|
34       klass.new(*props.map { |p| val[p] })
35     end
36   end
37 end
38
39 module Redwood
40   VERSION = "git"
41
42   BASE_DIR   = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
43   CONFIG_FN  = File.join(BASE_DIR, "config.yaml")
44   COLOR_FN   = File.join(BASE_DIR, "colors.yaml")
45   SOURCE_FN  = File.join(BASE_DIR, "sources.yaml")
46   LABEL_FN   = File.join(BASE_DIR, "labels.txt")
47   CONTACT_FN = File.join(BASE_DIR, "contacts.txt")
48   DRAFT_DIR  = File.join(BASE_DIR, "drafts")
49   SENT_FN    = File.join(BASE_DIR, "sent.mbox")
50   LOCK_FN    = File.join(BASE_DIR, "lock")
51   SUICIDE_FN = File.join(BASE_DIR, "please-kill-yourself")
52   HOOK_DIR   = File.join(BASE_DIR, "hooks")
53
54   YAML_DOMAIN = "masanjin.net"
55   YAML_DATE = "2006-10-01"
56
57   DEFAULT_INDEX = 'ferret'
58
59   ## record exceptions thrown in threads nicely
60   @exceptions = []
61   @exception_mutex = Mutex.new
62
63   attr_reader :exceptions
64   def record_exception e, name
65     @exception_mutex.synchronize do
66       @exceptions ||= []
67       @exceptions << [e, name]
68     end
69   end
70
71   def reporting_thread name
72     if $opts[:no_threads]
73       yield
74     else
75       ::Thread.new do
76         begin
77           yield
78         rescue Exception => e
79           record_exception e, name
80         end
81       end
82     end
83   end
84
85   module_function :reporting_thread, :record_exception, :exceptions
86
87 ## one-stop shop for yamliciousness
88   def save_yaml_obj object, fn, safe=false
89     if safe
90       safe_fn = "#{File.dirname fn}/safe_#{File.basename fn}"
91       mode = File.stat(fn).mode if File.exists? fn
92       File.open(safe_fn, "w", mode) { |f| f.puts object.to_yaml }
93       FileUtils.mv safe_fn, fn
94     else
95       File.open(fn, "w") { |f| f.puts object.to_yaml }
96     end
97   end
98
99   def load_yaml_obj fn, compress=false
100     if File.exists? fn
101       if compress
102         Zlib::GzipReader.open(fn) { |f| YAML::load f }
103       else
104         YAML::load_file fn
105       end
106     end
107   end
108
109   def start
110     Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
111     Redwood::ContactManager.init Redwood::CONTACT_FN
112     Redwood::LabelManager.init Redwood::LABEL_FN
113     Redwood::AccountManager.init $config[:accounts]
114     Redwood::DraftManager.init Redwood::DRAFT_DIR
115     Redwood::UpdateManager.init
116     Redwood::PollManager.init
117     Redwood::SuicideManager.init Redwood::SUICIDE_FN
118     Redwood::CryptoManager.init
119     Redwood::UndoManager.init
120     Redwood::SourceManager.init
121   end
122
123   def finish
124     Redwood::LabelManager.save if Redwood::LabelManager.instantiated?
125     Redwood::ContactManager.save if Redwood::ContactManager.instantiated?
126     Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated?
127   end
128
129   ## not really a good place for this, so I'll just dump it here.
130   ##
131   ## a source error is either a FatalSourceError or an OutOfSyncSourceError.
132   ## the superclass SourceError is just a generic.
133   def report_broken_sources opts={}
134     return unless BufferManager.instantiated?
135
136     broken_sources = SourceManager.sources.select { |s| s.error.is_a? FatalSourceError }
137     unless broken_sources.empty?
138       BufferManager.spawn_unless_exists("Broken source notification for #{broken_sources.join(',')}", opts) do
139         TextMode.new(<<EOM)
140 Source error notification
141 -------------------------
142
143 Hi there. It looks like one or more message sources is reporting
144 errors. Until this is corrected, messages from these sources cannot
145 be viewed, and new messages will not be detected.
146
147 #{broken_sources.map { |s| "Source: " + s.to_s + "\n Error: " + s.error.message.wrap(70).join("\n        ")}.join("\n\n")}
148 EOM
149 #' stupid ruby-mode
150       end
151     end
152
153     desynced_sources = SourceManager.sources.select { |s| s.error.is_a? OutOfSyncSourceError }
154     unless desynced_sources.empty?
155       BufferManager.spawn_unless_exists("Out-of-sync source notification for #{broken_sources.join(',')}", opts) do
156         TextMode.new(<<EOM)
157 Out-of-sync source notification
158 -------------------------------
159
160 Hi there. It looks like one or more sources has fallen out of sync
161 with my index. This can happen when you modify these sources with
162 other email clients. (Sorry, I don't play well with others.)
163
164 Until this is corrected, messages from these sources cannot be viewed,
165 and new messages will not be detected. Luckily, this is easy to correct!
166
167 #{desynced_sources.map do |s|
168   "Source: " + s.to_s + 
169    "\n Error: " + s.error.message.wrap(70).join("\n        ") + 
170    "\n   Fix: sup-sync --changed #{s.to_s}"
171   end}
172 EOM
173 #' stupid ruby-mode
174       end
175     end
176   end
177
178   module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
179                   :report_broken_sources
180 end
181
182 ## set up default configuration file
183 if File.exists? Redwood::CONFIG_FN
184   $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
185 else
186   require 'etc'
187   require 'socket'
188   name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first rescue nil
189   name ||= ENV["USER"]
190   email = ENV["USER"] + "@" + 
191     begin
192       Socket.gethostbyname(Socket.gethostname).first
193     rescue SocketError
194       Socket.gethostname
195     end
196
197   $config = {
198     :accounts => {
199       :default => {
200         :name => name,
201         :email => email,
202         :alternates => [],
203         :sendmail => "/usr/sbin/sendmail -oem -ti",
204         :signature => File.join(ENV["HOME"], ".signature")
205       }
206     },
207     :editor => ENV["EDITOR"] || "/usr/bin/vim -f -c 'setlocal spell spelllang=en_us' -c 'set filetype=mail'",
208     :thread_by_subject => false,
209     :edit_signature => false,
210     :ask_for_cc => true,
211     :ask_for_bcc => false,
212     :ask_for_subject => true,
213     :confirm_no_attachments => true,
214     :confirm_top_posting => true,
215     :discard_snippets_from_encrypted_messages => false,
216     :default_attachment_save_dir => "",
217     :sent_source => "sup://sent"
218   }
219   begin
220     FileUtils.mkdir_p Redwood::BASE_DIR
221     Redwood::save_yaml_obj $config, Redwood::CONFIG_FN
222   rescue StandardError => e
223     $stderr.puts "warning: #{e.message}"
224   end
225 end
226
227 require "sup/util"
228 require "sup/hook"
229
230 ## we have to initialize this guy first, because other classes must
231 ## reference it in order to register hooks, and they do that at parse
232 ## time.
233 Redwood::HookManager.init Redwood::HOOK_DIR
234
235 ## everything we need to get logging working
236 require "sup/buffer"
237 require "sup/keymap"
238 require "sup/mode"
239 require "sup/modes/scroll-mode"
240 require "sup/modes/text-mode"
241 require "sup/modes/log-mode"
242 require "sup/logger"
243 module Redwood
244   def log s; Logger.log s; end
245   module_function :log
246 end
247
248 ## determine encoding and character set
249   $encoding = Locale.current.charset
250   if $encoding
251     Redwood::log "using character set encoding #{$encoding.inspect}"
252   else
253     Redwood::log "warning: can't find character set by using locale, defaulting to utf-8"
254     $encoding = "UTF-8"
255   end
256
257 ## now everything else (which can feel free to call Redwood::log at load time)
258 require "sup/update"
259 require "sup/suicide"
260 require "sup/message-chunks"
261 require "sup/message"
262 require "sup/source"
263 require "sup/mbox"
264 require "sup/maildir"
265 require "sup/imap"
266 require "sup/person"
267 require "sup/account"
268 require "sup/thread"
269 require "sup/index"
270 require "sup/textfield"
271 require "sup/colormap"
272 require "sup/label"
273 require "sup/contact"
274 require "sup/tagger"
275 require "sup/draft"
276 require "sup/poll"
277 require "sup/crypto"
278 require "sup/undo"
279 require "sup/horizontal-selector"
280 require "sup/modes/line-cursor-mode"
281 require "sup/modes/help-mode"
282 require "sup/modes/edit-message-mode"
283 require "sup/modes/compose-mode"
284 require "sup/modes/resume-mode"
285 require "sup/modes/forward-mode"
286 require "sup/modes/reply-mode"
287 require "sup/modes/label-list-mode"
288 require "sup/modes/contact-list-mode"
289 require "sup/modes/thread-view-mode"
290 require "sup/modes/thread-index-mode"
291 require "sup/modes/label-search-results-mode"
292 require "sup/modes/search-results-mode"
293 require "sup/modes/person-search-results-mode"
294 require "sup/modes/inbox-mode"
295 require "sup/modes/buffer-list-mode"
296 require "sup/modes/poll-mode"
297 require "sup/modes/file-browser-mode"
298 require "sup/modes/completion-mode"
299 require "sup/sent"
300
301 $:.each do |base|
302   d = File.join base, "sup/share/modes/"
303   Redwood::Mode.load_all_modes d if File.directory? d
304 end