]> git.notmuchmail.org Git - notmuch/blob - vim/notmuch.vim
vim: don't automatically refresh after tagging
[notmuch] / vim / notmuch.vim
1 if exists("g:loaded_notmuch_rb")
2         finish
3 endif
4
5 if !has("ruby") || version < 700
6         finish
7 endif
8
9 let g:loaded_notmuch_rb = "yep"
10
11 let g:notmuch_rb_folders_maps = {
12         \ '<Enter>':    'folders_show_search()',
13         \ 's':          'folders_search_prompt()',
14         \ '=':          'folders_refresh()',
15         \ }
16
17 let g:notmuch_rb_search_maps = {
18         \ 'q':          'kill_this_buffer()',
19         \ '<Enter>':    'search_show_thread(1)',
20         \ '<Space>':    'search_show_thread(2)',
21         \ 'A':          'search_tag("-inbox -unread")',
22         \ 'I':          'search_tag("-unread")',
23         \ 't':          'search_tag("")',
24         \ 's':          'search_search_prompt()',
25         \ '=':          'search_refresh()',
26         \ '?':          'search_info()',
27         \ }
28
29 let g:notmuch_rb_show_maps = {
30         \ 'q':          'kill_this_buffer()',
31         \ 'A':          'show_tag("-inbox -unread")',
32         \ 'I':          'show_tag("-unread")',
33         \ 't':          'show_tag("")',
34         \ 'o':          'show_open_msg()',
35         \ 'e':          'show_extract_msg()',
36         \ 's':          'show_save_msg()',
37         \ 'r':          'show_reply()',
38         \ '?':          'show_info()',
39         \ '<Tab>':      'show_next_msg()',
40         \ }
41
42 let g:notmuch_rb_compose_maps = {
43         \ ',s':         'compose_send()',
44         \ ',q':         'compose_quit()',
45         \ }
46
47 let s:notmuch_rb_folders_default = [
48         \ [ 'new', 'tag:inbox and tag:unread' ],
49         \ [ 'inbox', 'tag:inbox' ],
50         \ [ 'unread', 'tag:unread' ],
51         \ ]
52
53 let s:notmuch_rb_date_format_default = '%d.%m.%y'
54 let s:notmuch_rb_datetime_format_default = '%d.%m.%y %H:%M:%S'
55 let s:notmuch_rb_reader_default = 'mutt -f %s'
56 let s:notmuch_rb_sendmail_default = 'sendmail'
57 let s:notmuch_rb_folders_count_threads_default = 0
58
59 if !exists('g:notmuch_rb_date_format')
60         let g:notmuch_rb_date_format = s:notmuch_rb_date_format_default
61 endif
62
63 if !exists('g:notmuch_rb_datetime_format')
64         let g:notmuch_rb_datetime_format = s:notmuch_rb_datetime_format_default
65 endif
66
67 if !exists('g:notmuch_rb_reader')
68         let g:notmuch_rb_reader = s:notmuch_rb_reader_default
69 endif
70
71 if !exists('g:notmuch_rb_sendmail')
72         let g:notmuch_rb_sendmail = s:notmuch_rb_sendmail_default
73 endif
74
75 if !exists('g:notmuch_rb_folders_count_threads')
76         let g:notmuch_rb_folders_count_threads = s:notmuch_rb_folders_count_threads_default
77 endif
78
79 function! s:new_file_buffer(type, fname)
80         exec printf('edit %s', a:fname)
81         execute printf('set filetype=notmuch-%s', a:type)
82         execute printf('set syntax=notmuch-%s', a:type)
83         ruby $buf_queue.push($curbuf.number)
84 endfunction
85
86 function! s:compose_unload()
87         if b:compose_done
88                 return
89         endif
90         if input('[s]end/[q]uit? ') =~ '^s'
91                 call s:compose_send()
92         endif
93 endfunction
94
95 "" actions
96
97 function! s:compose_quit()
98         let b:compose_done = 1
99         call s:kill_this_buffer()
100 endfunction
101
102 function! s:compose_send()
103         let b:compose_done = 1
104         let fname = expand('%')
105
106         " remove headers
107         0,4d
108         write
109
110         let cmdtxt = g:notmuch_sendmail . ' -t -f ' . s:reply_from . ' < ' . fname
111         let out = system(cmdtxt)
112         let err = v:shell_error
113         if err
114                 undo
115                 write
116                 echohl Error
117                 echo 'Eeek! unable to send mail'
118                 echo out
119                 echohl None
120                 return
121         endif
122         call delete(fname)
123         echo 'Mail sent successfully.'
124         call s:kill_this_buffer()
125 endfunction
126
127 function! s:show_next_msg()
128 ruby << EOF
129         r, c = $curwin.cursor
130         n = $curbuf.line_number
131         i = $messages.index { |m| n >= m.start && n <= m.end }
132         m = $messages[i + 1]
133         if m
134                 r = m.body_start + 1
135                 VIM::command("normal #{m.start}zt")
136                 $curwin.cursor = r, c
137         end
138 EOF
139 endfunction
140
141 function! s:show_reply()
142         ruby open_reply get_message.mail
143         let b:compose_done = 0
144         call s:set_map(g:notmuch_rb_compose_maps)
145         autocmd BufUnload <buffer> call s:compose_unload()
146         startinsert!
147 endfunction
148
149 function! s:show_info()
150         ruby vim_puts get_message.inspect
151 endfunction
152
153 function! s:show_extract_msg()
154 ruby << EOF
155         m = get_message
156         m.mail.attachments.each do |a|
157                 File.open(a.filename, 'w') do |f|
158                         f.write a.body.decoded
159                         print "Extracted '#{a.filename}'"
160                 end
161         end
162 EOF
163 endfunction
164
165 function! s:show_open_msg()
166 ruby << EOF
167         m = get_message
168         mbox = File.expand_path('~/.notmuch/vim_mbox')
169         cmd = VIM::evaluate('g:notmuch_rb_reader') % mbox
170         system "notmuch show --format=mbox id:#{m.message_id} > #{mbox} && #{cmd}"
171 EOF
172 endfunction
173
174 function! s:show_save_msg()
175         let file = input('File name: ')
176 ruby << EOF
177         file = VIM::evaluate('file')
178         m = get_message
179         system "notmuch show --format=mbox id:#{m.message_id} > #{file}"
180 EOF
181 endfunction
182
183 function! s:show_tag(intags)
184         if empty(a:intags)
185                 let tags = input('tags: ')
186         else
187                 let tags = a:intags
188         endif
189         ruby do_tag(get_cur_view, VIM::evaluate('l:tags'))
190         call s:show_next_thread()
191 endfunction
192
193 function! s:search_search_prompt()
194         let text = input('Search: ')
195         setlocal modifiable
196 ruby << EOF
197         $cur_search = VIM::evaluate('text')
198         search_render($cur_search)
199 EOF
200         setlocal nomodifiable
201 endfunction
202
203 function! s:search_info()
204         ruby vim_puts get_thread_id
205 endfunction
206
207 function! s:search_refresh()
208         setlocal modifiable
209         ruby search_render($cur_search)
210         setlocal nomodifiable
211 endfunction
212
213 function! s:search_tag(intags)
214         if empty(a:intags)
215                 let tags = input('tags: ')
216         else
217                 let tags = a:intags
218         endif
219         ruby do_tag(get_thread_id, VIM::evaluate('l:tags'))
220         norm j
221 endfunction
222
223 function! s:folders_search_prompt()
224         let text = input('Search: ')
225         call s:search(text)
226 endfunction
227
228 function! s:folders_refresh()
229         setlocal modifiable
230         ruby folders_render()
231         setlocal nomodifiable
232 endfunction
233
234 "" basic
235
236 function! s:show_cursor_moved()
237 ruby << EOF
238         if $render.is_ready?
239                 VIM::command('setlocal modifiable')
240                 $render.do_next
241                 VIM::command('setlocal nomodifiable')
242         end
243 EOF
244 endfunction
245
246 function! s:show_next_thread()
247         call s:kill_this_buffer()
248         if line('.') != line('$')
249                 norm j
250                 call s:search_show_thread(0)
251         else
252                 echo 'No more messages.'
253         endif
254 endfunction
255
256 function! s:kill_this_buffer()
257         bdelete!
258 ruby << EOF
259         $buf_queue.pop
260         b = $buf_queue.last
261         VIM::command("buffer #{b}") if b
262 EOF
263 endfunction
264
265 function! s:set_map(maps)
266         nmapclear <buffer>
267         for [key, code] in items(a:maps)
268                 let cmd = printf(":call <SID>%s<CR>", code)
269                 exec printf('nnoremap <buffer> %s %s', key, cmd)
270         endfor
271 endfunction
272
273 function! s:new_buffer(type)
274         enew
275         setlocal buftype=nofile bufhidden=hide
276         keepjumps 0d
277         execute printf('set filetype=notmuch-%s', a:type)
278         execute printf('set syntax=notmuch-%s', a:type)
279         ruby $buf_queue.push($curbuf.number)
280 endfunction
281
282 function! s:set_menu_buffer()
283         setlocal nomodifiable
284         setlocal cursorline
285         setlocal nowrap
286 endfunction
287
288 "" main
289
290 function! s:show(thread_id)
291         call s:new_buffer('show')
292         setlocal modifiable
293 ruby << EOF
294         thread_id = VIM::evaluate('a:thread_id')
295         $cur_thread = thread_id
296         $messages.clear
297         $curbuf.render do |b|
298                 do_read do |db|
299                         q = db.query(get_cur_view)
300                         q.sort = 0
301                         msgs = q.search_messages
302                         msgs.each do |msg|
303                                 m = Mail.read(msg.filename)
304                                 part = m.find_first_text
305                                 nm_m = Message.new(msg, m)
306                                 $messages << nm_m
307                                 date_fmt = VIM::evaluate('g:notmuch_rb_datetime_format')
308                                 date = Time.at(msg.date).strftime(date_fmt)
309                                 nm_m.start = b.count
310                                 b << "%s %s (%s)" % [msg['from'], date, msg.tags]
311                                 b << "Subject: %s" % [msg['subject']]
312                                 b << "To: %s" % m['to']
313                                 b << "Cc: %s" % m['cc']
314                                 b << "Date: %s" % m['date']
315                                 nm_m.body_start = b.count
316                                 b << "--- %s ---" % part.mime_type
317                                 part.convert.each_line do |l|
318                                         b << l.chomp
319                                 end
320                                 b << ""
321                                 nm_m.end = b.count
322                         end
323                         b.delete(b.count)
324                 end
325         end
326         $messages.each_with_index do |msg, i|
327                 VIM::command("syntax region nmShowMsg#{i}Desc start='\\%%%il' end='\\%%%il' contains=@nmShowMsgDesc" % [msg.start, msg.start + 1])
328                 VIM::command("syntax region nmShowMsg#{i}Head start='\\%%%il' end='\\%%%il' contains=@nmShowMsgHead" % [msg.start + 1, msg.body_start])
329                 VIM::command("syntax region nmShowMsg#{i}Body start='\\%%%il' end='\\%%%dl' contains=@nmShowMsgBody" % [msg.body_start, msg.end])
330         end
331 EOF
332         setlocal nomodifiable
333         call s:set_map(g:notmuch_rb_show_maps)
334 endfunction
335
336 function! s:search_show_thread(mode)
337 ruby << EOF
338         mode = VIM::evaluate('a:mode')
339         id = get_thread_id
340         case mode
341         when 0;
342         when 1; $cur_filter = nil
343         when 2; $cur_filter = $cur_search
344         end
345         VIM::command("call s:show('#{id}')")
346 EOF
347 endfunction
348
349 function! s:search(search)
350         call s:new_buffer('search')
351 ruby << EOF
352         $cur_search = VIM::evaluate('a:search')
353         search_render($cur_search)
354 EOF
355         call s:set_menu_buffer()
356         call s:set_map(g:notmuch_rb_search_maps)
357         autocmd CursorMoved <buffer> call s:show_cursor_moved()
358 endfunction
359
360 function! s:folders_show_search()
361 ruby << EOF
362         n = $curbuf.line_number
363         s = $searches[n - 1]
364         VIM::command("call s:search('#{s}')")
365 EOF
366 endfunction
367
368 function! s:folders()
369         call s:new_buffer('folders')
370         ruby folders_render()
371         call s:set_menu_buffer()
372         call s:set_map(g:notmuch_rb_folders_maps)
373 endfunction
374
375 "" root
376
377 function! s:set_defaults()
378         if exists('g:notmuch_rb_custom_search_maps')
379                 call extend(g:notmuch_rb_search_maps, g:notmuch_rb_custom_search_maps)
380         endif
381
382         if exists('g:notmuch_rb_custom_show_maps')
383                 call extend(g:notmuch_rb_show_maps, g:notmuch_rb_custom_show_maps)
384         endif
385
386         " TODO for now lets check the old folders too
387         if !exists('g:notmuch_rb_folders')
388                 if exists('g:notmuch_folders')
389                         let g:notmuch_rb_folders = g:notmuch_folders
390                 else
391                         let g:notmuch_rb_folders = s:notmuch_rb_folders_default
392                 endif
393         endif
394 endfunction
395
396 function! s:NotMuch()
397         call s:set_defaults()
398
399 ruby << EOF
400         require 'notmuch'
401         require 'rubygems'
402         require 'tempfile'
403         begin
404                 require 'mail'
405         rescue LoadError
406         end
407
408         $db_name = nil
409         $email_address = nil
410         $searches = []
411         $buf_queue = []
412         $threads = []
413         $messages = []
414         $config = {}
415         $mail_installed = defined?(Mail)
416
417         def get_config
418                 group = nil
419                 config = ENV['NOTMUCH_CONFIG'] || '~/.notmuch-config'
420                 File.open(File.expand_path(config)).each do |l|
421                         l.chomp!
422                         case l
423                         when /^\[(.*)\]$/
424                                 group = $1
425                         when ''
426                         when /^(.*)=(.*)$/
427                                 key = "%s.%s" % [group, $1]
428                                 value = $2
429                                 $config[key] = value
430                         end
431                 end
432
433                 $db_name = $config['database.path']
434                 $email_address = "%s <%s>" % [$config['user.name'], $config['user.primary_email']]
435         end
436
437         def vim_puts(s)
438                 VIM::command("echo '#{s.to_s}'")
439         end
440
441         def vim_p(s)
442                 VIM::command("echo '#{s.inspect}'")
443         end
444
445         def author_filter(a)
446                 # TODO email format, aliases
447                 a.strip!
448                 a.gsub!(/[\.@].*/, '')
449                 a.gsub!(/^ext /, '')
450                 a.gsub!(/ \(.*\)/, '')
451                 a
452         end
453
454         def get_thread_id
455                 n = $curbuf.line_number - 1
456                 return "thread:%s" % $threads[n]
457         end
458
459         def get_message
460                 n = $curbuf.line_number
461                 return $messages.find { |m| n >= m.start && n <= m.end }
462         end
463
464         def get_cur_view
465                 if $cur_filter
466                         return "#{$cur_thread} and (#{$cur_filter})"
467                 else
468                         return $cur_thread
469                 end
470         end
471
472         def do_write
473                 db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE)
474                 begin
475                         yield db
476                 ensure
477                         db.close
478                 end
479         end
480
481         def do_read
482                 db = Notmuch::Database.new($db_name)
483                 begin
484                         yield db
485                 ensure
486                         db.close
487                 end
488         end
489
490         def open_reply(orig)
491                 help_lines = [
492                         'Notmuch-Help: Type in your message here; to help you use these bindings:',
493                         'Notmuch-Help:   ,s    - send the message (Notmuch-Help lines will be removed)',
494                         'Notmuch-Help:   ,q    - abort the message',
495                         ]
496                 reply = orig.reply do |m|
497                         # fix headers
498                         if not m[:reply_to]
499                                 m.to = [orig[:from].to_s, orig[:to].to_s]
500                         end
501                         m.cc = orig[:cc]
502                         m.from = $email_address
503                         m.charset = 'utf-8'
504                         m.content_transfer_encoding = '7bit'
505                 end
506
507                 dir = File.expand_path('~/.notmuch/compose')
508                 FileUtils.mkdir_p(dir)
509                 Tempfile.open(['nm-', '.mail'], dir) do |f|
510                         lines = []
511
512                         lines += help_lines
513                         lines << ''
514
515                         body_lines = []
516                         if $mail_installed
517                                 addr = Mail::Address.new(orig[:from].value)
518                                 name = addr.name
519                                 name = addr.local + "@" if name.nil? && !addr.local.nil?
520                         else
521                                 name = orig[:from]
522                         end
523                         name = "somebody" if name.nil?
524
525                         body_lines << "%s wrote:" % name
526                         part = orig.find_first_text
527                         part.convert.each_line do |l|
528                                 body_lines << "> %s" % l.chomp
529                         end
530                         body_lines << ""
531                         body_lines << ""
532                         body_lines << ""
533
534                         reply.body = body_lines.join("\n")
535
536                         lines += reply.to_s.lines.map { |e| e.chomp }
537                         lines << ""
538
539                         old_count = lines.count - 1
540
541                         f.puts(lines)
542
543                         sig_file = File.expand_path('~/.signature')
544                         if File.exists?(sig_file)
545                                 f.puts("-- ")
546                                 f.write(File.read(sig_file))
547                         end
548
549                         f.flush
550
551                         VIM::command("let s:reply_from='%s'" % reply.from.first.to_s)
552                         VIM::command("call s:new_file_buffer('compose', '#{f.path}')")
553                         VIM::command("call cursor(#{old_count}, 0)")
554                 end
555         end
556
557         def folders_render()
558                 $curbuf.render do |b|
559                         folders = VIM::evaluate('g:notmuch_rb_folders')
560                         count_threads = VIM::evaluate('g:notmuch_rb_folders_count_threads')
561                         $searches.clear
562                         do_read do |db|
563                                 folders.each do |name, search|
564                                         q = db.query(search)
565                                         $searches << search
566                                         count = count_threads ? q.search_threads.count : q.search_messages.count
567                                         b << "%9d %-20s (%s)" % [count, name, search]
568                                 end
569                         end
570                 end
571         end
572
573         def search_render(search)
574                 date_fmt = VIM::evaluate('g:notmuch_rb_date_format')
575                 db = Notmuch::Database.new($db_name)
576                 q = db.query(search)
577                 $threads.clear
578                 t = q.search_threads
579
580                 $render = $curbuf.render_staged(t) do |b, items|
581                         items.each do |e|
582                                 authors = e.authors.to_utf8.split(/[,|]/).map { |a| author_filter(a) }.join(",")
583                                 date = Time.at(e.newest_date).strftime(date_fmt)
584                                 if $mail_installed
585                                         subject = Mail::Field.new("Subject: " + e.subject).to_s
586                                 else
587                                         subject = e.subject.force_encoding('utf-8')
588                                 end
589                                 b << "%-12s %3s %-20.20s | %s (%s)" % [date, e.matched_messages, authors, subject, e.tags]
590                                 $threads << e.thread_id
591                         end
592                 end
593         end
594
595         def do_tag(filter, tags)
596                 do_write do |db|
597                         q = db.query(filter)
598                         q.search_messages.each do |e|
599                                 e.freeze
600                                 tags.split.each do |t|
601                                         case t
602                                         when /^-(.*)/
603                                                 e.remove_tag($1)
604                                         when /^\+(.*)/
605                                                 e.add_tag($1)
606                                         when /^([^\+^-].*)/
607                                                 e.add_tag($1)
608                                         end
609                                 end
610                                 e.thaw
611                                 e.tags_to_maildir_flags
612                         end
613                 end
614         end
615
616         class Message
617                 attr_accessor :start, :body_start, :end
618                 attr_reader :message_id, :filename, :mail
619
620                 def initialize(msg, mail)
621                         @message_id = msg.message_id
622                         @filename = msg.filename
623                         @mail = mail
624                         @start = 0
625                         @end = 0
626                         mail.import_headers(msg) if not $mail_installed
627                 end
628
629                 def to_s
630                         "id:%s" % @message_id
631                 end
632
633                 def inspect
634                         "id:%s, file:%s" % [@message_id, @filename]
635                 end
636         end
637
638         class StagedRender
639                 def initialize(buffer, enumerable, block)
640                         @b = buffer
641                         @enumerable = enumerable
642                         @block = block
643                         @last_render = 0
644
645                         @b.render { do_next }
646                 end
647
648                 def is_ready?
649                         @last_render - @b.line_number <= $curwin.height
650                 end
651
652                 def do_next
653                         items = @enumerable.take($curwin.height * 2)
654                         return if items.empty?
655                         @block.call @b, items
656                         @last_render = @b.count
657                 end
658         end
659
660         class VIM::Buffer
661                 def <<(a)
662                         append(count(), a)
663                 end
664
665                 def render_staged(enumerable, &block)
666                         StagedRender.new(self, enumerable, block)
667                 end
668
669                 def render
670                         old_count = count
671                         yield self
672                         (1..old_count).each do
673                                 delete(1)
674                         end
675                 end
676         end
677
678         class Notmuch::Tags
679                 def to_s
680                         to_a.join(" ")
681                 end
682         end
683
684         class Notmuch::Message
685                 def to_s
686                         "id:%s" % message_id
687                 end
688         end
689
690         # workaround for bug in vim's ruby
691         class Object
692                 def flush
693                 end
694         end
695
696         module SimpleMessage
697                 class Header < Array
698                         def self.parse(string)
699                                 return nil if string.empty?
700                                 return Header.new(string.split(/,\s+/))
701                         end
702
703                         def to_s
704                                 self.join(', ')
705                         end
706                 end
707
708                 def initialize(string = nil)
709                         @raw_source = string
710                         @body = nil
711                         @headers = {}
712
713                         return if not string
714
715                         if string =~ /(.*?(\r\n|\n))\2/m
716                                 head, body = $1, $' || '', $2
717                         else
718                                 head, body = string, ''
719                         end
720                         @body = body
721                 end
722
723                 def [](name)
724                         @headers[name.to_sym]
725                 end
726
727                 def []=(name, value)
728                         @headers[name.to_sym] = value
729                 end
730
731                 def format_header(value)
732                         value.to_s.tr('_', '-').gsub(/(\w+)/) { $1.capitalize }
733                 end
734
735                 def to_s
736                         buffer = ''
737                         @headers.each do |key, value|
738                                 buffer << "%s: %s\r\n" %
739                                         [format_header(key), value]
740                         end
741                         buffer << "\r\n"
742                         buffer << @body
743                         buffer
744                 end
745
746                 def body=(value)
747                         @body = value
748                 end
749
750                 def from
751                         @headers[:from]
752                 end
753
754                 def decoded
755                         @body
756                 end
757
758                 def mime_type
759                         'text/plain'
760                 end
761
762                 def multipart?
763                         false
764                 end
765
766                 def reply
767                         r = Mail::Message.new
768                         r[:from] = self[:to]
769                         r[:to] = self[:from]
770                         r[:cc] = self[:cc]
771                         r[:in_reply_to] = self[:message_id]
772                         r[:references] = self[:references]
773                         r
774                 end
775
776                 HEADERS = [ :from, :to, :cc, :references, :in_reply_to, :reply_to, :message_id ]
777
778                 def import_headers(m)
779                         HEADERS.each do |e|
780                                 dashed = format_header(e)
781                                 @headers[e] = Header.parse(m[dashed])
782                         end
783                 end
784         end
785
786         module Mail
787
788                 if not $mail_installed
789                         puts "WARNING: Install the 'mail' gem, without it support is limited"
790
791                         def self.read(filename)
792                                 Message.new(File.open(filename, 'rb') { |f| f.read })
793                         end
794
795                         class Message
796                                 include SimpleMessage
797                         end
798                 end
799
800                 class Message
801
802                         def find_first_text
803                                 return self if not multipart?
804                                 return text_part || html_part
805                         end
806
807                         def convert
808                                 if mime_type != "text/html"
809                                         text = decoded
810                                 else
811                                         IO.popen("elinks --dump", "w+") do |pipe|
812                                                 pipe.write(decode_body)
813                                                 pipe.close_write
814                                                 text = pipe.read
815                                         end
816                                 end
817                                 text
818                         end
819                 end
820         end
821
822         class String
823                 def to_utf8
824                         RUBY_VERSION >= "1.9" ? force_encoding('utf-8') : self
825                 end
826         end
827
828         get_config
829 EOF
830         call s:folders()
831 endfunction
832
833 command NotMuch :call s:NotMuch()
834
835 " vim: set noexpandtab: