diff options
| author | David Bremner <bremner@debian.org> | 2023-12-01 07:51:09 -0400 |
|---|---|---|
| committer | David Bremner <bremner@debian.org> | 2023-12-01 07:51:09 -0400 |
| commit | 126347b6942dd4b0291beb67b119431ebd750a2a (patch) | |
| tree | 532c5163cb0972c8b9e6c8b4577b86afb9c6a6a2 /vim | |
Import notmuch_0.38.2.orig.tar.xz
[dgit import orig notmuch_0.38.2.orig.tar.xz]
Diffstat (limited to 'vim')
| -rw-r--r-- | vim/Makefile | 15 | ||||
| -rw-r--r-- | vim/README | 62 | ||||
| -rw-r--r-- | vim/notmuch.txt | 153 | ||||
| -rw-r--r-- | vim/notmuch.vim | 973 | ||||
| -rw-r--r-- | vim/notmuch.yaml | 10 | ||||
| -rw-r--r-- | vim/syntax/notmuch-compose.vim | 7 | ||||
| -rw-r--r-- | vim/syntax/notmuch-folders.vim | 12 | ||||
| -rw-r--r-- | vim/syntax/notmuch-git-diff.vim | 26 | ||||
| -rw-r--r-- | vim/syntax/notmuch-search.vim | 12 | ||||
| -rw-r--r-- | vim/syntax/notmuch-show.vim | 24 |
10 files changed, 1294 insertions, 0 deletions
diff --git a/vim/Makefile b/vim/Makefile new file mode 100644 index 00000000..b6f9db78 --- /dev/null +++ b/vim/Makefile @@ -0,0 +1,15 @@ +prefix = $(HOME)/.vim + +INSTALL = install -v -D -m644 +D = $(DESTDIR) + +all: + @echo "Nothing to build" + +install: + $(INSTALL) $(CURDIR)/notmuch.vim $(D)$(prefix)/plugin/notmuch.vim + $(INSTALL) $(CURDIR)/notmuch.txt $(D)$(prefix)/doc/notmuch.txt + @$(foreach file,$(wildcard syntax/*), \ + $(INSTALL) $(CURDIR)/$(file) $(D)$(prefix)/$(file);) + +.PHONY: all install diff --git a/vim/README b/vim/README new file mode 100644 index 00000000..777c20c0 --- /dev/null +++ b/vim/README @@ -0,0 +1,62 @@ +== notmuch vim ruby == + +This is a vim plug-in that provides a fully usable mail client interface, +utilizing the notmuch framework, through it's ruby bindings. + +== install == + +Simply run 'make install'. However, check that you have the dependencies below. + +=== vim +ruby === + +Make sure your vim version has ruby support: check for +ruby in 'vim --version' +features. + +=== ruby bindings === + +Check if you are able to run the following command cleanly: + + % ruby -e "require 'notmuch'" + +If you don't see any errors, it means it's working and you can go to the next +section. + +If it's not, you would need to compile them. Go to the 'bindings/ruby' +directory in the notmuch source tree. + +=== mail gem === + +Since libnotmuch library concentrates on things other than handling mail, we +need a library to do that, and for Ruby the best library for that is called +'mail'. The easiest way to install it is with ruby's gem. In most distro's the +package is called 'rubygems'. + +Once you have gem, run: + + % gem install mail + +In some systems gems are installed on a per-user basis by default, so make sure +you are running as the same user as the one that installed them. + +This gem is not mandatory, but it's extremely recommended. + +== Running == + +Simple: + + % gvim -c ':NotMuch' + +Enjoy ;) + +== More stuff == + +As an example to configure a key mapping to add the tag 'to-do' and archive, +this is what I use: + +let g:notmuch_custom_search_maps = { + \ 't': 'search_tag("+to-do -inbox")', + \ } + +let g:notmuch_custom_show_maps = { + \ 't': 'show_tag("+to-do -inbox")', + \ } diff --git a/vim/notmuch.txt b/vim/notmuch.txt new file mode 100644 index 00000000..c98f2b53 --- /dev/null +++ b/vim/notmuch.txt @@ -0,0 +1,153 @@ +*notmuch.txt* Plug-in to make vim a nice email client using notmuch + +Author: Felipe Contreras <felipe.contreras@gmail.com> + +Overview |notmuch-intro| +Usage |notmuch-usage| +Mappings |notmuch-mappings| +Configuration |notmuch-config| + +============================================================================== +OVERVIEW *notmuch-intro* + +This is a vim plug-in that provides a fully usable mail client interface, +utilizing the notmuch framework. + +It has three main views: folders, search, and thread. In the folder view you +can find a summary of saved searches, In the search view you can see all the +threads that comprise the selected search, and in the thread view you can read +every mail in the thread. + +============================================================================== +USAGE *notmuch-usage* + +To use it, simply run the `:NotMuch` command. + +By default you start in the folder view which shows you default searches and +the number of threads that match those: +> + 10 new (tag:inbox and tag:unread) + 20 inbox (tag:inbox) + 30 unread (tag:unread) +< +You can see the threads of each by clicking `enter`, which sends you to the +search view. In both the search and folder views you can type `s` to type a +new search, or `=` to refresh. To see a thread, type `enter` again. + +To exit a view, click `q`. + +Also, you can specify a search directly: +> + :NotMuch is:inbox and date:yesterday.. +< +============================================================================== +MAPPINGS *notmuch-mappings* + +------------------------------------------------------------------------------ +Folder view~ + +<enter> Show selected search +s Enter a new search += Refresh +c Compose a new mail + +------------------------------------------------------------------------------ +Search view~ + +q Quit view +<enter> Show selected search +<space> Show selected search with filter +A Archive (-inbox -unread) +I Mark as read (-unread) +t Tag (prompted) +s Search += Refresh +? Show search information +c Compose a new mail +> +------------------------------------------------------------------------------ +Thread view~ + +q Quit view +A Archive (-inbox -unread) +I Mark as read (-unread) +t Tag (prompted) +s Search +p Save patches +r Reply +? Show thread information +<tab> Show next message +c Compose a new mail + +------------------------------------------------------------------------------ +Compose view~ + +q Quit view +s Send + +============================================================================== +CONFIGURATION *notmuch-config* + +You can add the following configurations to your `.vimrc`, or +`~/.vim/after/plugin/notmuch.vim`. + + *g:notmuch_folders* + +The first thing you might want to do is set your custom searches. +> + let g:notmuch_folders = [ + \ [ 'new', 'tag:inbox and tag:unread' ], + \ [ 'inbox', 'tag:inbox' ], + \ [ 'unread', 'tag:unread' ], + \ [ 'to-do', 'tag:to-do' ], + \ [ 'to-me', 'to:john.doe and tag:new' ], + \ ] +< + + *g:notmuch_custom_search_maps* + *g:notmuch_custom_show_maps* + +You can also configure the keyboard mappings for the different views: +> + let g:notmuch_custom_search_maps = { + \ 't': 'search_tag("+to-do -inbox")', + \ 'd': 'search_tag("+deleted -inbox -unread")', + \ } + + let g:notmuch_custom_show_maps = { + \ 't': 'show_tag("+to-do -inbox")', + \ 'd': 'show_tag("+deleted -inbox -unread")', + \ } +< + + *g:notmuch_date_format* + +To configure the date format you want in the search view: +> + let g:notmuch_date_format = '%d.%m.%y' +< + + *g:notmuch_datetime_format* + +You can do the same for the thread view: +> + let g:notmuch_datetime_format = '%d.%m.%y %H:%M:%S' +< + + *g:notmuch_folders_count_threads* + +If you want to count the threads instead of the messages in the folder view: +> + let g:notmuch_folders_count_threads = 1 +< + + *g:notmuch_reader* + *g:notmuch_sendmail* + +You can also configure your external mail reader and sendmail program: +> + let g:notmuch_reader = 'mutt -f %s' + let g:notmuch_sendmail = 'sendmail' +< + +vim:tw=78:ts=8:noet:ft=help: diff --git a/vim/notmuch.vim b/vim/notmuch.vim new file mode 100644 index 00000000..c1c2f63d --- /dev/null +++ b/vim/notmuch.vim @@ -0,0 +1,973 @@ +if exists("g:loaded_notmuch") + finish +endif + +if !has("ruby") || version < 700 + finish +endif + +let g:loaded_notmuch = "yep" + +let g:notmuch_folders_maps = { + \ '<Enter>': 'folders_show_search()', + \ 's': 'folders_search_prompt()', + \ '=': 'folders_refresh()', + \ 'c': 'compose()', + \ } + +let g:notmuch_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()', + \ 'c': 'compose()', + \ } + +let g:notmuch_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()', + \ 'p': 'show_save_patches()', + \ 'r': 'show_reply()', + \ '?': 'show_info()', + \ '<Tab>': 'show_next_msg()', + \ 'c': 'compose()', + \ } + +let g:notmuch_compose_maps = { + \ ',s': 'compose_send()', + \ ',q': 'compose_quit()', + \ } + +let s:notmuch_folders_default = [ + \ [ 'new', 'tag:inbox and tag:unread' ], + \ [ 'inbox', 'tag:inbox' ], + \ [ 'unread', 'tag:unread' ], + \ ] + +let s:notmuch_date_format_default = '%d.%m.%y' +let s:notmuch_datetime_format_default = '%d.%m.%y %H:%M:%S' +let s:notmuch_reader_default = 'mutt -f %s' +let s:notmuch_sendmail_default = 'sendmail' +let s:notmuch_folders_count_threads_default = 0 +let s:notmuch_compose_start_insert_default = 1 + +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 $curbuf.init(VIM::evaluate('a:type')) +endfunction + +function! s:on_compose_delete() + 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('%') + let lines = getline(5, '$') + +ruby << EOF + # Generate proper mail to send + text = VIM::evaluate('lines').join("\n") + fname = VIM::evaluate('fname') + transport = Mail.new(text) + transport.message_id = generate_message_id + transport.charset = 'utf-8' + File.write(fname, transport.to_s) +EOF + + let cmdtxt = g:notmuch_sendmail . ' -t -f ' . s:reply_from . ' < ' . fname + let out = system(cmdtxt) + let err = v:shell_error + if err + 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_compose_maps) + autocmd BufDelete <buffer> call s:on_compose_delete() + if g:notmuch_compose_start_insert + startinsert! + end +endfunction + +function! s:compose() + ruby open_compose + let b:compose_done = 0 + call s:set_map(g:notmuch_compose_maps) + autocmd BufDelete <buffer> call s:on_compose_delete() + if g:notmuch_compose_start_insert + startinsert! + end +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_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_save_patches() +ruby << EOF + q = $curbuf.query($cur_thread) + t = q.search_threads.first + n = 0 + t.toplevel_messages.first.replies.each do |m| + next if not m['subject'] =~ /^\[PATCH.*\]/ + file = "%04d.patch" % [n += 1] + system "notmuch show --format=mbox id:#{m.message_id} > #{file}" + end + vim_puts "Saved #{n} patches" +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: ') + if text == "" + return + endif + setlocal modifiable +ruby << EOF + $cur_search = VIM::evaluate('text') + $curbuf.reopen + 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 $curbuf.reopen + 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 +endfunction + +function! s:folders_search_prompt() + let text = input('Search: ') + call s:search(text) +endfunction + +function! s:folders_refresh() + setlocal modifiable + ruby $curbuf.reopen + 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() +ruby << EOF + $curbuf.close + VIM::command("bdelete!") +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 $curbuf.init(VIM::evaluate('a:type')) +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| + q = $curbuf.query(get_cur_view) + q.sort = Notmuch::SORT_OLDEST_FIRST + $exclude_tags.each { |t| + q.add_tag_exclude(t) + } + 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_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" % msg['to'] + b << "Cc: %s" % msg['cc'] + b << "Date: %s" % msg['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 + $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_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_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_folders_maps) +endfunction + +"" root + +function! s:set_defaults() + if !exists('g:notmuch_date_format') + if exists('g:notmuch_rb_date_format') + let g:notmuch_date_format = g:notmuch_rb_date_format + else + let g:notmuch_date_format = s:notmuch_date_format_default + endif + endif + + if !exists('g:notmuch_datetime_format') + if exists('g:notmuch_rb_datetime_format') + let g:notmuch_datetime_format = g:notmuch_rb_datetime_format + else + let g:notmuch_datetime_format = s:notmuch_datetime_format_default + endif + endif + + if !exists('g:notmuch_reader') + if exists('g:notmuch_rb_reader') + let g:notmuch_reader = g:notmuch_rb_reader + else + let g:notmuch_reader = s:notmuch_reader_default + endif + endif + + if !exists('g:notmuch_sendmail') + if exists('g:notmuch_rb_sendmail') + let g:notmuch_sendmail = g:notmuch_rb_sendmail + else + let g:notmuch_sendmail = s:notmuch_sendmail_default + endif + endif + + if !exists('g:notmuch_folders_count_threads') + if exists('g:notmuch_rb_count_threads') + let g:notmuch_count_threads = g:notmuch_rb_count_threads + else + let g:notmuch_folders_count_threads = s:notmuch_folders_count_threads_default + endif + endif + + if !exists('g:notmuch_compose_start_insert') + let g:notmuch_compose_start_insert = s:notmuch_compose_start_insert_default + endif + + if !exists('g:notmuch_custom_search_maps') && exists('g:notmuch_rb_custom_search_maps') + let g:notmuch_custom_search_maps = g:notmuch_rb_custom_search_maps + endif + + if !exists('g:notmuch_custom_show_maps') && exists('g:notmuch_rb_custom_show_maps') + let g:notmuch_custom_show_maps = g:notmuch_rb_custom_show_maps + endif + + if exists('g:notmuch_custom_search_maps') + call extend(g:notmuch_search_maps, g:notmuch_custom_search_maps) + endif + + if exists('g:notmuch_custom_show_maps') + call extend(g:notmuch_show_maps, g:notmuch_custom_show_maps) + endif + + if !exists('g:notmuch_folders') + if exists('g:notmuch_rb_folders') + let g:notmuch_folders = g:notmuch_rb_folders + else + let g:notmuch_folders = s:notmuch_folders_default + endif + endif +endfunction + +function! s:NotMuch(...) + call s:set_defaults() + +ruby << EOF + require 'notmuch' + require 'rubygems' + require 'tempfile' + require 'socket' + begin + require 'mail' + rescue LoadError + end + + $db_name = nil + $email = $email_name = $email_address = nil + $exclude_tags = [] + $searches = [] + $threads = [] + $messages = [] + $mail_installed = defined?(Mail) + + def get_config_item(item) + result = '' + IO.popen(['notmuch', 'config', 'get', item]) { |out| + result = out.read + } + return result.rstrip + end + + def get_config + $db_name = get_config_item('database.path') + $email_name = get_config_item('user.name') + $email_address = get_config_item('user.primary_email') + $email_name = get_config_item('user.name') + $email = "%s <%s>" % [$email_name, $email_address] + ignore_tags = get_config_item('search.exclude_tags') + $exclude_tags = ignore_tags.split("\n") + 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 generate_message_id + t = Time.now + random_tag = sprintf('%x%x_%x%x%x', + t.to_i, t.tv_usec, + $$, Thread.current.object_id.abs, rand(255)) + return "<#{random_tag}@#{Socket.gethostname}.notmuch>" + end + + def open_compose_helper(lines, cur) + 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', + ] + + dir = File.expand_path('~/.notmuch/compose') + FileUtils.mkdir_p(dir) + Tempfile.open(['nm-', '.mail'], dir) do |f| + f.puts(help_lines) + f.puts + 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 + + cur += help_lines.size + 1 + + VIM::command("let s:reply_from='%s'" % $email_address) + VIM::command("call s:new_file_buffer('compose', '#{f.path}')") + VIM::command("call cursor(#{cur}, 0)") + end + end + + def open_reply(orig) + 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 + m.charset = 'utf-8' + end + + 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.present.lines.map { |e| e.chomp } + lines << "" + + cur = lines.count - 1 + + open_compose_helper(lines, cur) + end + + def open_compose() + lines = [] + + lines << "From: #{$email}" + lines << "To: " + cur = lines.count + + lines << "Cc: " + lines << "Bcc: " + lines << "Subject: " + lines << "" + lines << "" + lines << "" + + open_compose_helper(lines, cur) + end + + def folders_render() + $curbuf.render do |b| + folders = VIM::evaluate('g:notmuch_folders') + count_threads = VIM::evaluate('g:notmuch_folders_count_threads') == 1 + $searches.clear + folders.each do |name, search| + q = $curbuf.query(search) + $exclude_tags.each { |t| + q.add_tag_exclude(t) + } + $searches << search + count = count_threads ? q.count_threads : q.count_messages + b << "%9d %-20s (%s)" % [count, name, search] + end + end + end + + def search_render(search) + date_fmt = VIM::evaluate('g:notmuch_date_format') + q = $curbuf.query(search) + q.sort = Notmuch::SORT_NEWEST_FIRST + $exclude_tags.each { |t| + q.add_tag_exclude(t) + } + $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) + subject = e.messages.first['subject'] + if $mail_installed + subject = Mail::Field.parse("Subject: " + subject).to_s + else + subject = 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) + $curbuf.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 + q.destroy! + end + end + + module DbHelper + def init(name) + @name = name + @db = Notmuch::Database.new($db_name) + @queries = [] + end + + def query(*args) + q = @db.query(*args) + @queries << q + q + end + + def close + @queries.delete_if { |q| ! q.destroy! } + @db.close + end + + def reopen + close if @db + @db = Notmuch::Database.new($db_name) + end + + def do_write + db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE) + begin + yield db + ensure + db.close + 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 + include DbHelper + + 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(VIM::evaluate('exists("g:notmuch_html_converter") ? ' + + 'g:notmuch_html_converter : "elinks --dump"'), "w+") do |pipe| + pipe.write(decode_body) + pipe.close_write + text = pipe.read + end + end + text + end + + def present + buffer = '' + header.fields.each do |f| + buffer << "%s: %s\r\n" % [f.name, f.to_s] + end + buffer << "\r\n" + buffer << body.to_s + buffer + end + end + end + + class String + def to_utf8 + RUBY_VERSION >= "1.9" ? force_encoding('utf-8') : self + end + end + + get_config +EOF + if a:0 + call s:search(join(a:000)) + else + call s:folders() + endif +endfunction + +command -nargs=* NotMuch call s:NotMuch(<f-args>) + +" vim: set noexpandtab: diff --git a/vim/notmuch.yaml b/vim/notmuch.yaml new file mode 100644 index 00000000..6f3b705c --- /dev/null +++ b/vim/notmuch.yaml @@ -0,0 +1,10 @@ +addon: notmuch +description: "notmuch mail user interface" +files: + - plugin/notmuch.vim + - doc/notmuch.txt + - syntax/notmuch-compose.vim + - syntax/notmuch-folders.vim + - syntax/notmuch-git-diff.vim + - syntax/notmuch-search.vim + - syntax/notmuch-show.vim diff --git a/vim/syntax/notmuch-compose.vim b/vim/syntax/notmuch-compose.vim new file mode 100644 index 00000000..19adb756 --- /dev/null +++ b/vim/syntax/notmuch-compose.vim @@ -0,0 +1,7 @@ +runtime! syntax/mail.vim + +syntax region nmComposeHelp contains=nmComposeHelpLine start='^Notmuch-Help:\%1l' end='^\(Notmuch-Help:\)\@!' +syntax match nmComposeHelpLine /Notmuch-Help:/ contained + +highlight link nmComposeHelp Include +highlight link nmComposeHelpLine Error diff --git a/vim/syntax/notmuch-folders.vim b/vim/syntax/notmuch-folders.vim new file mode 100644 index 00000000..9477f86f --- /dev/null +++ b/vim/syntax/notmuch-folders.vim @@ -0,0 +1,12 @@ +" notmuch folders mode syntax file + +syntax region nmFoldersCount start='^' end='\%10v' +syntax region nmFoldersName start='\%11v' end='\%31v' +syntax match nmFoldersSearch /([^()]\+)$/ + +highlight link nmFoldersCount Statement +highlight link nmFoldersName Type +highlight link nmFoldersSearch String + +highlight CursorLine term=reverse cterm=reverse gui=reverse + diff --git a/vim/syntax/notmuch-git-diff.vim b/vim/syntax/notmuch-git-diff.vim new file mode 100644 index 00000000..6f15fdc7 --- /dev/null +++ b/vim/syntax/notmuch-git-diff.vim @@ -0,0 +1,26 @@ +syn match diffRemoved "^-.*" +syn match diffAdded "^+.*" + +syn match diffSeparator "^---$" +syn match diffSubname " @@..*"ms=s+3 contained +syn match diffLine "^@.*" contains=diffSubname + +syn match diffFile "^diff .*" +syn match diffNewFile "^+++ .*" +syn match diffOldFile "^--- .*" + +hi def link diffOldFile diffFile +hi def link diffNewFile diffFile + +hi def link diffFile Type +hi def link diffRemoved Special +hi def link diffAdded Identifier +hi def link diffLine Statement +hi def link diffSubname PreProc + +syntax match gitDiffStatLine /^ .\{-}\zs[+-]\+$/ contains=gitDiffStatAdd,gitDiffStatDelete +syntax match gitDiffStatAdd /+/ contained +syntax match gitDiffStatDelete /-/ contained + +hi def link gitDiffStatAdd diffAdded +hi def link gitDiffStatDelete diffRemoved diff --git a/vim/syntax/notmuch-search.vim b/vim/syntax/notmuch-search.vim new file mode 100644 index 00000000..f458d778 --- /dev/null +++ b/vim/syntax/notmuch-search.vim @@ -0,0 +1,12 @@ +syntax region nmSearch start=/^/ end=/$/ oneline contains=nmSearchDate +syntax match nmSearchDate /^.\{-13}/ contained nextgroup=nmSearchNum +syntax match nmSearchNum /.\{-4}/ contained nextgroup=nmSearchFrom +syntax match nmSearchFrom /.\{-21}/ contained nextgroup=nmSearchSubject +syntax match nmSearchSubject /.\{0,}\(([^()]\+)$\)\@=/ contained nextgroup=nmSearchTags +syntax match nmSearchTags /.\+$/ contained + +highlight link nmSearchDate Statement +highlight link nmSearchNum Type +highlight link nmSearchFrom Include +highlight link nmSearchSubject Normal +highlight link nmSearchTags String diff --git a/vim/syntax/notmuch-show.vim b/vim/syntax/notmuch-show.vim new file mode 100644 index 00000000..c3a98b77 --- /dev/null +++ b/vim/syntax/notmuch-show.vim @@ -0,0 +1,24 @@ +" notmuch show mode syntax file + +syntax cluster nmShowMsgDesc contains=nmShowMsgDescWho,nmShowMsgDescDate,nmShowMsgDescTags +syntax match nmShowMsgDescWho /[^)]\+)/ contained +syntax match nmShowMsgDescDate / ([^)]\+[0-9]) / contained +syntax match nmShowMsgDescTags /([^)]\+)$/ contained + +syntax cluster nmShowMsgHead contains=nmShowMsgHeadKey,nmShowMsgHeadVal +syntax match nmShowMsgHeadKey /^[^:]\+: / contained +syntax match nmShowMsgHeadVal /^\([^:]\+: \)\@<=.*/ contained + +syntax cluster nmShowMsgBody contains=@nmShowMsgBodyMail,@nmShowMsgBodyGit +syntax include @nmShowMsgBodyMail syntax/mail.vim + +silent! syntax include @nmShowMsgBodyGit syntax/notmuch-git-diff.vim + +highlight nmShowMsgDescWho term=reverse cterm=reverse gui=reverse +highlight link nmShowMsgDescDate Type +highlight link nmShowMsgDescTags String + +highlight link nmShowMsgHeadKey Macro +"highlight link nmShowMsgHeadVal NONE + +highlight Folded term=reverse ctermfg=LightGrey ctermbg=Black guifg=LightGray guibg=Black |
