diff options
author | Felipe Contreras <felipe.contreras@gmail.com> | 2013-06-02 19:31:59 -0500 |
---|---|---|
committer | Felipe Contreras <felipe.contreras@gmail.com> | 2013-06-02 19:42:43 -0500 |
commit | 138c6aa098fe4236dc334cc55ac4fc414ddb298c (patch) | |
tree | 1521462482deec7317ed5234d5a87c1091261842 /vim/notmuch.vim | |
parent | dbc3a247cac72e400efb45250e10eebe64722904 (diff) |
Add new notmuch vim plugin
The old one was not properly maintained and is now deprecated. The new
one has much better support.
Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
Diffstat (limited to 'vim/notmuch.vim')
-rw-r--r-- | vim/notmuch.vim | 836 |
1 files changed, 836 insertions, 0 deletions
diff --git a/vim/notmuch.vim b/vim/notmuch.vim new file mode 100644 index 00000000..e0fc1400 --- /dev/null +++ b/vim/notmuch.vim @@ -0,0 +1,836 @@ +if exists("g:loaded_notmuch_rb") + finish +endif + +if !has("ruby") || version < 700 + finish +endif + +let g:loaded_notmuch_rb = "yep" + +let g:notmuch_rb_folders_maps = { + \ '<Enter>': 'folders_show_search()', + \ 's': 'folders_search_prompt()', + \ '=': 'folders_refresh()', + \ } + +let g:notmuch_rb_search_maps = { + \ 'q': 'kill_this_buffer()', + \ '<Enter>': 'search_show_thread(1)', + \ '<Space>': 'search_show_thread(2)', + \ 'A': 'search_tag("-inbox -unread")', + \ 'I': 'search_tag("-unread")', + \ 't': 'search_tag("")', + \ 's': 'search_search_prompt()', + \ '=': 'search_refresh()', + \ '?': 'search_info()', + \ } + +let g:notmuch_rb_show_maps = { + \ 'q': 'kill_this_buffer()', + \ 'A': 'show_tag("-inbox -unread")', + \ 'I': 'show_tag("-unread")', + \ 't': 'show_tag("")', + \ 'o': 'show_open_msg()', + \ 'e': 'show_extract_msg()', + \ 's': 'show_save_msg()', + \ 'r': 'show_reply()', + \ '?': 'show_info()', + \ '<Tab>': 'show_next_msg()', + \ } + +let g:notmuch_rb_compose_maps = { + \ ',s': 'compose_send()', + \ ',q': 'compose_quit()', + \ } + +let s:notmuch_rb_folders_default = [ + \ [ 'new', 'tag:inbox and tag:unread' ], + \ [ 'inbox', 'tag:inbox' ], + \ [ 'unread', 'tag:unread' ], + \ ] + +let s:notmuch_rb_date_format_default = '%d.%m.%y' +let s:notmuch_rb_datetime_format_default = '%d.%m.%y %H:%M:%S' +let s:notmuch_rb_reader_default = 'xfce4-terminal -e "mutt -f %s"' +let s:notmuch_rb_sendmail_default = 'sendmail' +let s:notmuch_rb_folders_count_threads_default = 0 + +if !exists('g:notmuch_rb_date_format') + let g:notmuch_rb_date_format = s:notmuch_rb_date_format_default +endif + +if !exists('g:notmuch_rb_datetime_format') + let g:notmuch_rb_datetime_format = s:notmuch_rb_datetime_format_default +endif + +if !exists('g:notmuch_rb_reader') + let g:notmuch_rb_reader = s:notmuch_rb_reader_default +endif + +if !exists('g:notmuch_rb_sendmail') + let g:notmuch_rb_sendmail = s:notmuch_rb_sendmail_default +endif + +if !exists('g:notmuch_rb_folders_count_threads') + let g:notmuch_rb_folders_count_threads = s:notmuch_rb_folders_count_threads_default +endif + +function! s:new_file_buffer(type, fname) + exec printf('edit %s', a:fname) + execute printf('set filetype=notmuch-%s', a:type) + execute printf('set syntax=notmuch-%s', a:type) + ruby $buf_queue.push($curbuf.number) +endfunction + +function! s:compose_unload() + if b:compose_done + return + endif + if input('[s]end/[q]uit? ') =~ '^s' + call s:compose_send() + endif +endfunction + +"" actions + +function! s:compose_quit() + let b:compose_done = 1 + call s:kill_this_buffer() +endfunction + +function! s:compose_send() + let b:compose_done = 1 + let fname = expand('%') + + " remove headers + 0,4d + write + + let cmdtxt = g:notmuch_sendmail . ' -t -f ' . s:reply_from . ' < ' . fname + let out = system(cmdtxt) + let err = v:shell_error + if err + undo + write + echohl Error + echo 'Eeek! unable to send mail' + echo out + echohl None + return + endif + call delete(fname) + echo 'Mail sent successfully.' + call s:kill_this_buffer() +endfunction + +function! s:show_next_msg() +ruby << EOF + r, c = $curwin.cursor + n = $curbuf.line_number + i = $messages.index { |m| n >= m.start && n <= m.end } + m = $messages[i + 1] + if m + r = m.body_start + 1 + VIM::command("normal #{m.start}zt") + $curwin.cursor = r, c + end +EOF +endfunction + +function! s:show_reply() + ruby open_reply get_message.mail + let b:compose_done = 0 + call s:set_map(g:notmuch_rb_compose_maps) + autocmd BufUnload <buffer> call s:compose_unload() + startinsert! +endfunction + +function! s:show_info() + ruby vim_puts get_message.inspect +endfunction + +function! s:show_extract_msg() +ruby << EOF + m = get_message + m.mail.attachments.each do |a| + File.open(a.filename, 'w') do |f| + f.write a.body.decoded + print "Extracted '#{a.filename}'" + end + end +EOF +endfunction + +function! s:show_open_msg() +ruby << EOF + m = get_message + mbox = File.expand_path('~/.notmuch/vim_mbox') + cmd = VIM::evaluate('g:notmuch_rb_reader') % mbox + system "notmuch show --format=mbox id:#{m.message_id} > #{mbox} && #{cmd}" +EOF +endfunction + +function! s:show_save_msg() + let file = input('File name: ') +ruby << EOF + file = VIM::evaluate('file') + m = get_message + system "notmuch show --format=mbox id:#{m.message_id} > #{file}" +EOF +endfunction + +function! s:show_tag(intags) + if empty(a:intags) + let tags = input('tags: ') + else + let tags = a:intags + endif + ruby do_tag(get_cur_view, VIM::evaluate('l:tags')) + call s:show_next_thread() +endfunction + +function! s:search_search_prompt() + let text = input('Search: ') + setlocal modifiable +ruby << EOF + $cur_search = VIM::evaluate('text') + search_render($cur_search) +EOF + setlocal nomodifiable +endfunction + +function! s:search_info() + ruby vim_puts get_thread_id +endfunction + +function! s:search_refresh() + setlocal modifiable + ruby search_render($cur_search) + setlocal nomodifiable +endfunction + +function! s:search_tag(intags) + if empty(a:intags) + let tags = input('tags: ') + else + let tags = a:intags + endif + ruby do_tag(get_thread_id, VIM::evaluate('l:tags')) + norm j + call s:search_refresh() +endfunction + +function! s:folders_search_prompt() + let text = input('Search: ') + call s:search(text) +endfunction + +function! s:folders_refresh() + setlocal modifiable + ruby folders_render() + setlocal nomodifiable +endfunction + +"" basic + +function! s:show_cursor_moved() +ruby << EOF + if $render.is_ready? + VIM::command('setlocal modifiable') + $render.do_next + VIM::command('setlocal nomodifiable') + end +EOF +endfunction + +function! s:show_next_thread() + call s:kill_this_buffer() + if line('.') != line('$') + norm j + call s:search_show_thread(0) + else + echo 'No more messages.' + endif +endfunction + +function! s:kill_this_buffer() + bdelete! +ruby << EOF + $buf_queue.pop + b = $buf_queue.last + VIM::command("buffer #{b}") if b +EOF +endfunction + +function! s:set_map(maps) + nmapclear <buffer> + for [key, code] in items(a:maps) + let cmd = printf(":call <SID>%s<CR>", code) + exec printf('nnoremap <buffer> %s %s', key, cmd) + endfor +endfunction + +function! s:new_buffer(type) + enew + setlocal buftype=nofile bufhidden=hide + keepjumps 0d + execute printf('set filetype=notmuch-%s', a:type) + execute printf('set syntax=notmuch-%s', a:type) + ruby $buf_queue.push($curbuf.number) +endfunction + +function! s:set_menu_buffer() + setlocal nomodifiable + setlocal cursorline + setlocal nowrap +endfunction + +"" main + +function! s:show(thread_id) + call s:new_buffer('show') + setlocal modifiable +ruby << EOF + thread_id = VIM::evaluate('a:thread_id') + $cur_thread = thread_id + $messages.clear + $curbuf.render do |b| + do_read do |db| + q = db.query(get_cur_view) + q.sort = 0 + msgs = q.search_messages + msgs.each do |msg| + m = Mail.read(msg.filename) + part = m.find_first_text + nm_m = Message.new(msg, m) + $messages << nm_m + date_fmt = VIM::evaluate('g:notmuch_rb_datetime_format') + date = Time.at(msg.date).strftime(date_fmt) + nm_m.start = b.count + b << "%s %s (%s)" % [msg['from'], date, msg.tags] + b << "Subject: %s" % [msg['subject']] + b << "To: %s" % m['to'] + b << "Cc: %s" % m['cc'] + b << "Date: %s" % m['date'] + nm_m.body_start = b.count + b << "--- %s ---" % part.mime_type + part.convert.each_line do |l| + b << l.chomp + end + b << "" + nm_m.end = b.count + end + b.delete(b.count) + end + end + $messages.each_with_index do |msg, i| + VIM::command("syntax region nmShowMsg#{i}Desc start='\\%%%il' end='\\%%%il' contains=@nmShowMsgDesc" % [msg.start, msg.start + 1]) + VIM::command("syntax region nmShowMsg#{i}Head start='\\%%%il' end='\\%%%il' contains=@nmShowMsgHead" % [msg.start + 1, msg.body_start]) + VIM::command("syntax region nmShowMsg#{i}Body start='\\%%%il' end='\\%%%dl' contains=@nmShowMsgBody" % [msg.body_start, msg.end]) + end +EOF + setlocal nomodifiable + call s:set_map(g:notmuch_rb_show_maps) +endfunction + +function! s:search_show_thread(mode) +ruby << EOF + mode = VIM::evaluate('a:mode') + id = get_thread_id + case mode + when 0; + when 1; $cur_filter = nil + when 2; $cur_filter = $cur_search + end + VIM::command("call s:show('#{id}')") +EOF +endfunction + +function! s:search(search) + call s:new_buffer('search') +ruby << EOF + $cur_search = VIM::evaluate('a:search') + search_render($cur_search) +EOF + call s:set_menu_buffer() + call s:set_map(g:notmuch_rb_search_maps) + autocmd CursorMoved <buffer> call s:show_cursor_moved() +endfunction + +function! s:folders_show_search() +ruby << EOF + n = $curbuf.line_number + s = $searches[n - 1] + VIM::command("call s:search('#{s}')") +EOF +endfunction + +function! s:folders() + call s:new_buffer('folders') + ruby folders_render() + call s:set_menu_buffer() + call s:set_map(g:notmuch_rb_folders_maps) +endfunction + +"" root + +function! s:set_defaults() + if exists('g:notmuch_rb_custom_search_maps') + call extend(g:notmuch_rb_search_maps, g:notmuch_rb_custom_search_maps) + endif + + if exists('g:notmuch_rb_custom_show_maps') + call extend(g:notmuch_rb_show_maps, g:notmuch_rb_custom_show_maps) + endif + + " TODO for now lets check the old folders too + if !exists('g:notmuch_rb_folders') + if exists('g:notmuch_folders') + let g:notmuch_rb_folders = g:notmuch_folders + else + let g:notmuch_rb_folders = s:notmuch_rb_folders_default + endif + endif +endfunction + +function! s:NotMuchR() + call s:set_defaults() + +ruby << EOF + require 'notmuch' + require 'rubygems' + require 'tempfile' + begin + require 'mail' + rescue LoadError + end + + $db_name = nil + $email_address = nil + $searches = [] + $buf_queue = [] + $threads = [] + $messages = [] + $config = {} + $mail_installed = defined?(Mail) + + def get_config + group = nil + config = ENV['NOTMUCH_CONFIG'] || '~/.notmuch-config' + File.open(File.expand_path(config)).each do |l| + l.chomp! + case l + when /^\[(.*)\]$/ + group = $1 + when '' + when /^(.*)=(.*)$/ + key = "%s.%s" % [group, $1] + value = $2 + $config[key] = value + end + end + + $db_name = $config['database.path'] + $email_address = "%s <%s>" % [$config['user.name'], $config['user.primary_email']] + end + + def vim_puts(s) + VIM::command("echo '#{s.to_s}'") + end + + def vim_p(s) + VIM::command("echo '#{s.inspect}'") + end + + def author_filter(a) + # TODO email format, aliases + a.strip! + a.gsub!(/[\.@].*/, '') + a.gsub!(/^ext /, '') + a.gsub!(/ \(.*\)/, '') + a + end + + def get_thread_id + n = $curbuf.line_number - 1 + return "thread:%s" % $threads[n] + end + + def get_message + n = $curbuf.line_number + return $messages.find { |m| n >= m.start && n <= m.end } + end + + def get_cur_view + if $cur_filter + return "#{$cur_thread} and (#{$cur_filter})" + else + return $cur_thread + end + end + + def do_write + db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE) + begin + yield db + ensure + db.close + end + end + + def do_read + db = Notmuch::Database.new($db_name) + begin + yield db + ensure + db.close + end + end + + def open_reply(orig) + help_lines = [ + 'Notmuch-Help: Type in your message here; to help you use these bindings:', + 'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)', + 'Notmuch-Help: ,q - abort the message', + ] + reply = orig.reply do |m| + # fix headers + if not m[:reply_to] + m.to = [orig[:from].to_s, orig[:to].to_s] + end + m.cc = orig[:cc] + m.from = $email_address + m.charset = 'utf-8' + m.content_transfer_encoding = '7bit' + end + + dir = File.expand_path('~/.notmuch/compose') + FileUtils.mkdir_p(dir) + Tempfile.open(['nm-', '.mail'], dir) do |f| + lines = [] + + lines += help_lines + lines << '' + + body_lines = [] + if $mail_installed + addr = Mail::Address.new(orig[:from].value) + name = addr.name + name = addr.local + "@" if name.nil? && !addr.local.nil? + else + name = orig[:from] + end + name = "somebody" if name.nil? + + body_lines << "%s wrote:" % name + part = orig.find_first_text + part.convert.each_line do |l| + body_lines << "> %s" % l.chomp + end + body_lines << "" + body_lines << "" + body_lines << "" + + reply.body = body_lines.join("\n") + + lines += reply.to_s.lines.map { |e| e.chomp } + lines << "" + + old_count = lines.count - 1 + + f.puts(lines) + + sig_file = File.expand_path('~/.signature') + if File.exists?(sig_file) + f.puts("-- ") + f.write(File.read(sig_file)) + end + + f.flush + + VIM::command("let s:reply_from='%s'" % reply.from.first.to_s) + VIM::command("call s:new_file_buffer('compose', '#{f.path}')") + VIM::command("call cursor(#{old_count}, 0)") + end + end + + def folders_render() + $curbuf.render do |b| + folders = VIM::evaluate('g:notmuch_rb_folders') + count_threads = VIM::evaluate('g:notmuch_rb_folders_count_threads') + $searches.clear + do_read do |db| + folders.each do |name, search| + q = db.query(search) + $searches << search + count = count_threads ? q.search_threads.count : q.search_messages.count + b << "%9d %-20s (%s)" % [count, name, search] + end + end + end + end + + def search_render(search) + date_fmt = VIM::evaluate('g:notmuch_rb_date_format') + db = Notmuch::Database.new($db_name) + q = db.query(search) + $threads.clear + t = q.search_threads + + $render = $curbuf.render_staged(t) do |b, items| + items.each do |e| + authors = e.authors.to_utf8.split(/[,|]/).map { |a| author_filter(a) }.join(",") + date = Time.at(e.newest_date).strftime(date_fmt) + if $mail_installed + subject = Mail::Field.new("Subject: " + e.subject).to_s + else + subject = e.subject.force_encoding('utf-8') + end + b << "%-12s %3s %-20.20s | %s (%s)" % [date, e.matched_messages, authors, subject, e.tags] + $threads << e.thread_id + end + end + end + + def do_tag(filter, tags) + do_write do |db| + q = db.query(filter) + q.search_messages.each do |e| + e.freeze + tags.split.each do |t| + case t + when /^-(.*)/ + e.remove_tag($1) + when /^\+(.*)/ + e.add_tag($1) + when /^([^\+^-].*)/ + e.add_tag($1) + end + end + e.thaw + e.tags_to_maildir_flags + end + end + end + + class Message + attr_accessor :start, :body_start, :end + attr_reader :message_id, :filename, :mail + + def initialize(msg, mail) + @message_id = msg.message_id + @filename = msg.filename + @mail = mail + @start = 0 + @end = 0 + mail.import_headers(msg) if not $mail_installed + end + + def to_s + "id:%s" % @message_id + end + + def inspect + "id:%s, file:%s" % [@message_id, @filename] + end + end + + class StagedRender + def initialize(buffer, enumerable, block) + @b = buffer + @enumerable = enumerable + @block = block + @last_render = 0 + + @b.render { do_next } + end + + def is_ready? + @last_render - @b.line_number <= $curwin.height + end + + def do_next + items = @enumerable.take($curwin.height * 2) + return if items.empty? + @block.call @b, items + @last_render = @b.count + end + end + + class VIM::Buffer + def <<(a) + append(count(), a) + end + + def render_staged(enumerable, &block) + StagedRender.new(self, enumerable, block) + end + + def render + old_count = count + yield self + (1..old_count).each do + delete(1) + end + end + end + + class Notmuch::Tags + def to_s + to_a.join(" ") + end + end + + class Notmuch::Message + def to_s + "id:%s" % message_id + end + end + + # workaround for bug in vim's ruby + class Object + def flush + end + end + + module SimpleMessage + class Header < Array + def self.parse(string) + return nil if string.empty? + return Header.new(string.split(/,\s+/)) + end + + def to_s + self.join(', ') + end + end + + def initialize(string = nil) + @raw_source = string + @body = nil + @headers = {} + + return if not string + + if string =~ /(.*?(\r\n|\n))\2/m + head, body = $1, $' || '', $2 + else + head, body = string, '' + end + @body = body + end + + def [](name) + @headers[name.to_sym] + end + + def []=(name, value) + @headers[name.to_sym] = value + end + + def format_header(value) + value.to_s.tr('_', '-').gsub(/(\w+)/) { $1.capitalize } + end + + def to_s + buffer = '' + @headers.each do |key, value| + buffer << "%s: %s\r\n" % + [format_header(key), value] + end + buffer << "\r\n" + buffer << @body + buffer + end + + def body=(value) + @body = value + end + + def from + @headers[:from] + end + + def decoded + @body + end + + def mime_type + 'text/plain' + end + + def multipart? + false + end + + def reply + r = Mail::Message.new + r[:from] = self[:to] + r[:to] = self[:from] + r[:cc] = self[:cc] + r[:in_reply_to] = self[:message_id] + r[:references] = self[:references] + r + end + + HEADERS = [ :from, :to, :cc, :references, :in_reply_to, :reply_to, :message_id ] + + def import_headers(m) + HEADERS.each do |e| + dashed = format_header(e) + @headers[e] = Header.parse(m[dashed]) + end + end + end + + module Mail + + if not $mail_installed + puts "WARNING: Install the 'mail' gem, without it support is limited" + + def self.read(filename) + Message.new(File.open(filename, 'rb') { |f| f.read }) + end + + class Message + include SimpleMessage + end + end + + class Message + + def find_first_text + return self if not multipart? + return text_part || html_part + end + + def convert + if mime_type != "text/html" + text = decoded + else + IO.popen("elinks --dump", "w+") do |pipe| + pipe.write(decode_body) + pipe.close_write + text = pipe.read + end + end + text + end + end + end + + class String + def to_utf8 + RUBY_VERSION >= "1.9" ? force_encoding('utf-8') : self + end + end + + get_config +EOF + call s:folders() +endfunction + +command NotMuchR :call s:NotMuchR() + +" vim: set noexpandtab: |