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