]> git.cworth.org Git - sup/blob - lib/sup.rb
Merge branch 'ncurses-fixes'
[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 o, fn, safe=false
89     o = if o.is_a?(Array)
90       o.map { |x| (x.respond_to?(:before_marshal) && x.before_marshal) || x }
91     else
92       o.respond_to?(:before_marshal) && o.before_marshal
93     end
94
95     if safe
96       safe_fn = "#{File.dirname fn}/safe_#{File.basename fn}"
97       mode = File.stat(fn).mode if File.exists? fn
98       File.open(safe_fn, "w", mode) { |f| f.puts o.to_yaml }
99       FileUtils.mv safe_fn, fn
100     else
101       File.open(fn, "w") { |f| f.puts o.to_yaml }
102     end
103   end
104
105   def load_yaml_obj fn, compress=false
106     o = if File.exists? fn
107       if compress
108         Zlib::GzipReader.open(fn) { |f| YAML::load f }
109       else
110         YAML::load_file fn
111       end
112     end
113     if o.is_a?(Array)
114       o.each { |x| x.after_unmarshal! if x.respond_to?(:after_unmarshal!) }
115     else
116       o.after_unmarshal! if o.respond_to?(:after_unmarshal!)
117     end
118     o
119   end
120
121   def start
122     Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
123     Redwood::ContactManager.init Redwood::CONTACT_FN
124     Redwood::LabelManager.init Redwood::LABEL_FN
125     Redwood::AccountManager.init $config[:accounts]
126     Redwood::DraftManager.init Redwood::DRAFT_DIR
127     Redwood::UpdateManager.init
128     Redwood::PollManager.init
129     Redwood::SuicideManager.init Redwood::SUICIDE_FN
130     Redwood::CryptoManager.init
131     Redwood::UndoManager.init
132     Redwood::SourceManager.init
133   end
134
135   def finish
136     Redwood::LabelManager.save if Redwood::LabelManager.instantiated?
137     Redwood::ContactManager.save if Redwood::ContactManager.instantiated?
138     Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated?
139   end
140
141   ## not really a good place for this, so I'll just dump it here.
142   ##
143   ## a source error is either a FatalSourceError or an OutOfSyncSourceError.
144   ## the superclass SourceError is just a generic.
145   def report_broken_sources opts={}
146     return unless BufferManager.instantiated?
147
148     broken_sources = SourceManager.sources.select { |s| s.error.is_a? FatalSourceError }
149     unless broken_sources.empty?
150       BufferManager.spawn_unless_exists("Broken source notification for #{broken_sources.join(',')}", opts) do
151         TextMode.new(<<EOM)
152 Source error notification
153 -------------------------
154
155 Hi there. It looks like one or more message sources is reporting
156 errors. Until this is corrected, messages from these sources cannot
157 be viewed, and new messages will not be detected.
158
159 #{broken_sources.map { |s| "Source: " + s.to_s + "\n Error: " + s.error.message.wrap(70).join("\n        ")}.join("\n\n")}
160 EOM
161 #' stupid ruby-mode
162       end
163     end
164
165     desynced_sources = SourceManager.sources.select { |s| s.error.is_a? OutOfSyncSourceError }
166     unless desynced_sources.empty?
167       BufferManager.spawn_unless_exists("Out-of-sync source notification for #{broken_sources.join(',')}", opts) do
168         TextMode.new(<<EOM)
169 Out-of-sync source notification
170 -------------------------------
171
172 Hi there. It looks like one or more sources has fallen out of sync
173 with my index. This can happen when you modify these sources with
174 other email clients. (Sorry, I don't play well with others.)
175
176 Until this is corrected, messages from these sources cannot be viewed,
177 and new messages will not be detected. Luckily, this is easy to correct!
178
179 #{desynced_sources.map do |s|
180   "Source: " + s.to_s + 
181    "\n Error: " + s.error.message.wrap(70).join("\n        ") + 
182    "\n   Fix: sup-sync --changed #{s.to_s}"
183   end}
184 EOM
185 #' stupid ruby-mode
186       end
187     end
188   end
189
190   module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
191                   :report_broken_sources
192 end
193
194 ## set up default configuration file
195 if File.exists? Redwood::CONFIG_FN
196   $config = Redwood::load_yaml_obj Redwood::CONFIG_FN
197 else
198   require 'etc'
199   require 'socket'
200   name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first rescue nil
201   name ||= ENV["USER"]
202   email = ENV["USER"] + "@" + 
203     begin
204       Socket.gethostbyname(Socket.gethostname).first
205     rescue SocketError
206       Socket.gethostname
207     end
208
209   $config = {
210     :accounts => {
211       :default => {
212         :name => name,
213         :email => email,
214         :alternates => [],
215         :sendmail => "/usr/sbin/sendmail -oem -ti",
216         :signature => File.join(ENV["HOME"], ".signature")
217       }
218     },
219     :editor => ENV["EDITOR"] || "/usr/bin/vim -f -c 'setlocal spell spelllang=en_us' -c 'set filetype=mail'",
220     :thread_by_subject => false,
221     :edit_signature => false,
222     :ask_for_cc => true,
223     :ask_for_bcc => false,
224     :ask_for_subject => true,
225     :confirm_no_attachments => true,
226     :confirm_top_posting => true,
227     :discard_snippets_from_encrypted_messages => false,
228     :default_attachment_save_dir => "",
229     :sent_source => "sup://sent"
230   }
231   begin
232     FileUtils.mkdir_p Redwood::BASE_DIR
233     Redwood::save_yaml_obj $config, Redwood::CONFIG_FN
234   rescue StandardError => e
235     $stderr.puts "warning: #{e.message}"
236   end
237 end
238
239 require "sup/util"
240 require "sup/hook"
241
242 ## we have to initialize this guy first, because other classes must
243 ## reference it in order to register hooks, and they do that at parse
244 ## time.
245 Redwood::HookManager.init Redwood::HOOK_DIR
246
247 ## everything we need to get logging working
248 require "sup/logger"
249 Redwood::Logger.init.add_sink $stderr
250 include Redwood::LogsStuff
251
252 ## determine encoding and character set
253   $encoding = Locale.current.charset
254   if $encoding
255     debug "using character set encoding #{$encoding.inspect}"
256   else
257     warn "can't find character set by using locale, defaulting to utf-8"
258     $encoding = "UTF-8"
259   end
260
261 require "sup/buffer"
262 require "sup/keymap"
263 require "sup/mode"
264 require "sup/modes/scroll-mode"
265 require "sup/modes/text-mode"
266 require "sup/modes/log-mode"
267 require "sup/update"
268 require "sup/suicide"
269 require "sup/message-chunks"
270 require "sup/message"
271 require "sup/source"
272 require "sup/mbox"
273 require "sup/maildir"
274 require "sup/imap"
275 require "sup/person"
276 require "sup/account"
277 require "sup/thread"
278 require "sup/index"
279 require "sup/textfield"
280 require "sup/colormap"
281 require "sup/label"
282 require "sup/contact"
283 require "sup/tagger"
284 require "sup/draft"
285 require "sup/poll"
286 require "sup/crypto"
287 require "sup/undo"
288 require "sup/horizontal-selector"
289 require "sup/modes/line-cursor-mode"
290 require "sup/modes/help-mode"
291 require "sup/modes/edit-message-mode"
292 require "sup/modes/compose-mode"
293 require "sup/modes/resume-mode"
294 require "sup/modes/forward-mode"
295 require "sup/modes/reply-mode"
296 require "sup/modes/label-list-mode"
297 require "sup/modes/contact-list-mode"
298 require "sup/modes/thread-view-mode"
299 require "sup/modes/thread-index-mode"
300 require "sup/modes/label-search-results-mode"
301 require "sup/modes/search-results-mode"
302 require "sup/modes/person-search-results-mode"
303 require "sup/modes/inbox-mode"
304 require "sup/modes/buffer-list-mode"
305 require "sup/modes/poll-mode"
306 require "sup/modes/file-browser-mode"
307 require "sup/modes/completion-mode"
308 require "sup/sent"
309
310 $:.each do |base|
311   d = File.join base, "sup/share/modes/"
312   Redwood::Mode.load_all_modes d if File.directory? d
313 end