" nm-vimpy.vim plugin --- run notmuch within vim " " This file is part of Notmuch. " " Notmuch is free software: you can redistribute it and/or modify it " under the terms of the GNU General Public License as published by " the Free Software Foundation, either version 3 of the License, or " (at your option) any later version. " " Notmuch is distributed in the hope that it will be useful, but " WITHOUT ANY WARRANTY; without even the implied warranty of " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU " General Public License for more details. " " You should have received a copy of the GNU General Public License " along with Notmuch. If not, see . " " Based on the notmuch.vim plugin by: " Authors: Bart Trojanowski " Contributors: Felipe Contreras , " Peter Hartman " " Mostly rewritten using python by Anton Khirnov " " --- initialization if exists('s:nm_vimpy_loaded') || &cp finish endif " init the python layer let s:python_path = expand(':p:h') python import sys exec "python sys.path += [r'" . s:python_path . "']" python import vim, nm_vim command! NMVimpy call NMVimpy() " --- configuration defaults let s:nm_vimpy_defaults = { \ 'g:notmuch_cmd': 'notmuch' , \ \ 'g:notmuch_search_newest_first': 1 , \ \ 'g:notmuch_compose_insert_mode_start': 1 , \ 'g:notmuch_compose_header_help': 1 , \ 'g:notmuch_compose_temp_file_dir': '~/.notmuch/compose/' , \ 'g:nm_vimpy_fcc_maildir': 'sent' , \ 'g:nm_vimpy_user_agent': 'notmuch-vimpy' , \ } " defaults for g:nm_vimpy_folders " override with: let g:nm_vimpy_folders = [ ... ] let s:nm_vimpy_folders_defaults = [ \ [ 'new', 'tag:inbox and tag:unread' ], \ [ 'inbox', 'tag:inbox' ], \ [ 'unread', 'tag:unread' ], \ ] let s:nm_vimpy_show_headers_defaults = [ \ 'From', \ 'To', \ 'Cc', \ 'Subject', \ 'Date', \ 'Reply-To', \ 'Message-Id', \] " defaults for g:nm_vimpy_compose_headers " override with: let g:nm_vimpy_compose_headers = [ ... ] let s:nm_vimpy_compose_headers_defaults = [ \ 'From', \ 'To', \ 'Cc', \ 'Bcc', \ 'Subject' \ ] " --- keyboard mapping definitions " --- --- bindings for folders mode {{{2 let g:nm_vimpy_folders_maps = { \ 'm': ':call NM_new_mail()', \ 's': ':call NM_search_prompt(0)', \ 'q': ':call NM_kill_this_buffer()', \ '=': ':call NM_folders_refresh_view()', \ '': ':call NM_folders_show_search('''')', \ '': ':call NM_folders_show_search(''tag:unread'')', \ 'tt': ':call NM_folders_from_tags()', \ } " --- --- bindings for search screen {{{2 let g:nm_vimpy_search_maps = { \ '': ':call NM_search_show_thread()', \ '': ':call NM_search_show_thread_unread()', \ '': ':call NM_search_expand('''')', \ 'a': ':call NM_search_archive_thread()', \ 'A': ':call NM_search_mark_read_then_archive_thread()', \ 'D': ':call NM_search_delete_thread()', \ 'f': ':call NM_search_filter()', \ 'm': ':call NM_new_mail()', \ 'o': ':call NM_search_toggle_order()', \ 'r': ':call NM_search_reply_to_thread()', \ 's': ':call NM_search_prompt(0)', \ ',s': ':call NM_search_prompt(1)', \ 'q': ':call NM_kill_this_buffer()', \ '+': ':call NM_search_add_tags([])', \ '-': ':call NM_search_remove_tags([])', \ '=': ':call NM_search_refresh_view()', \ } " --- --- bindings for show screen {{{2 let g:nm_vimpy_show_maps = { \ '': ':call NM_jump_message(-1)', \ '': ':call NM_jump_message(+1)', \ '': ':call NM_search_expand('''')', \ 'q': ':call NM_kill_this_buffer()', \ 's': ':call NM_search_prompt(0)', \ \ \ 'a': ':call NM_show_archive_thread()', \ 'A': ':call NM_show_mark_read_then_archive_thread()', \ 'N': ':call NM_show_mark_read_then_next_open_message()', \ 'v': ':call NM_show_view_all_mime_parts()', \ '+': ':call NM_show_add_tag()', \ '-': ':call NM_show_remove_tag()', \ '': ':call NM_show_advance()', \ '\|': ':call NM_show_pipe_message()', \ \ '': ':call NM_show_view_attachment()', \ 'S': ':call NM_show_save_attachment()', \ \ 'r': ':call NM_show_reply()', \ 'R': ':call NM_show_view_raw_message()', \ 'm': ':call NM_new_mail()', \ } " --- --- bindings for compose screen {{{2 let g:nm_vimpy_compose_nmaps = { \ ',s': ':call NM_compose_send()', \ ',a': ':call NM_compose_attach()', \ ',q': ':call NM_kill_this_buffer()', \ '': ':call NM_compose_next_entry_area()', \ } let g:nm_vimpy_raw_message_nmaps = { \ 'q': ':call NM_kill_this_buffer()', \ } " --- implement folders screen {{{1 " Create the folders buffer. " Takes a list of [ folder name, query string] function! s:NM_cmd_folders(folders) call NM_create_buffer('folders') silent 0put!=':view :view unread s:search =:refresh tt:all tags m:new mail' python nm_vim.SavedSearches(vim.eval("a:folders")) call NM_finalize_menu_buffer() call NM_set_map('n', g:nm_vimpy_folders_maps) endfunction " Show a folder for each existing tag. function! s:NM_folders_from_tags() let folders = [] python nm_vim.vim_get_tags() for tag in split(taglist, '\n') call add(folders, [tag, 'tag:' . tag ]) endfor call NM_cmd_folders(folders) endfunction " --- --- folders screen action functions {{{2 " Refresh the folders screen function! s:NM_folders_refresh_view() let lno = line('.') setlocal modifiable silent norm 3GdG python nm_vim.get_current_buffer().refresh() setlocal nomodifiable exec printf('norm %dG', lno) endfunction " Show contents of the folder corresponding to current line AND query function! s:NM_folders_show_search(query) exec printf('python nm_vim.vim_get_object(%d, 0)', line('.')) if exists('obj') if len(a:query) let querystr = '(' . obj['id'] . ') and ' . a:query else let querystr = obj['id'] endif call NM_cmd_search(querystr, 0) endif endfunction " Create the search buffer corresponding to querystr. " If relative is 1, the search is relative to current buffer function! s:NM_cmd_search(querystr, relative) let cur_buf = bufnr('%') call NM_create_buffer('search') silent 0put!=printf(' Query results: %s', a:querystr) if a:relative python nm_vim.Search(querystr = vim.eval("a:querystr"), parent = nm_vim.nm_buffers[vim.eval('cur_buf')]) else python nm_vim.Search(querystr = vim.eval("a:querystr")) endif call NM_finalize_menu_buffer() call NM_set_map('n', g:nm_vimpy_search_maps) endfunction " --- --- search screen action functions {{{2 " Show the thread corresponding to current line function! s:NM_search_show_thread() let querystr = NM_search_thread_id() if len(querystr) call NM_cmd_show(querystr) endif endfunction " Same as NM_search_show_thread, except jump to first unread function! s:NM_search_show_thread_unread() call NM_search_show_thread() python nm_vim.vim_get_message_for_tag('unread') if start > 0 exec printf('norm %dGzt', start) silent! norm zo endif endfunction " Search according to input from user. " If edit is 1, current query string is inserted to prompt for editing. function! s:NM_search_prompt(edit) if a:edit python nm_vim.vim_get_id() else let buf_id = '' endif let querystr = input('Search: ', buf_id, 'custom,NM_search_type_completion') if len(querystr) call NM_cmd_search(querystr, 0) endif endfunction " Filter current search, i.e. search for " (current querystr) AND (user input) function! s:NM_search_filter() let querystr = input('Filter: ', '', 'custom,NM_search_type_completion') if len(querystr) call NM_cmd_search(querystr, 1) endif endfunction """"""""""""""""""""""'' TODO function! s:NM_search_archive_thread() call NM_tag([], ['-inbox']) norm j endfunction function! s:NM_search_mark_read_then_archive_thread() call NM_tag([], ['-unread', '-inbox']) norm j endfunction function! s:NM_search_delete_thread() call NM_tag([], ['+junk','-inbox','-unread']) norm j endfunction """"""""""""""""""""""""""""""""""""""""""""""""""""" " XXX This function is broken function! s:NM_search_toggle_order() let g:notmuch_search_newest_first = !g:notmuch_search_newest_first " FIXME: maybe this would be better done w/o reading re-reading the lines " reversing the b:nm_raw_lines and the buffer lines would be better call NM_search_refresh_view() endfunction "XXX this function is broken function! s:NM_search_reply_to_thread() python vim.command('let querystr = "%s"'%nm_vim.get_current_buffer().id) let cmd = ['reply'] call add(cmd, NM_search_thread_id()) call add(cmd, 'AND') call extend(cmd, [querystr]) let data = NM_run(cmd) let lines = split(data, "\n") call NM_newComposeBuffer(lines, 0) endfunction function! s:NM_search_add_tags(tags) call NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags) endfunction function! s:NM_search_remove_tags(tags) call NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags) endfunction function! s:NM_search_refresh_view() let lno = line('.') setlocal modifiable norm 3ggdG python nm_vim.get_current_buffer().refresh() setlocal nomodifiable " FIXME: should find the line of the thread we were on if possible exec printf('norm %dG', lno) endfunction " --- --- search screen helper functions {{{2 function! s:NM_search_thread_id() exec printf('python nm_vim.vim_get_object(%d, 0)', line('.')) if exists('obj') return 'thread:' . obj['id'] endif return '' endfunction function! s:NM_search_add_remove_tags(prompt, prefix, intags) if type(a:intags) != type([]) || len(a:intags) == 0 let text = input(a:prompt, '', 'custom,NM_tag_name_completion') if !strlen(text) return endif let tags = split(text, ' ') else let tags = a:intags endif call map(tags, 'a:prefix . v:val') call NM_tag([], tags) endfunction " --- implement show screen {{{1 function! s:NM_cmd_show(querystr) "TODO: folding, syntax call NM_create_buffer('show') python nm_vim.ShowThread(vim.eval('a:querystr')) call NM_set_map('n', g:nm_vimpy_show_maps) setlocal fillchars= setlocal foldtext=NM_show_foldtext() setlocal foldcolumn=6 setlocal foldmethod=syntax setlocal nomodifiable setlocal nowrap call NM_jump_message(1) endfunction function! s:NM_jump_message(offset) "TODO implement can_change_thread and find_matching, nicer positioning exec printf('python nm_vim.vim_get_object(%d, %d)', line('.'), a:offset) if exists('obj') silent! norm zc exec printf('norm %dGzt', obj['start']) silent! norm zo endif endfunction function! s:NM_show_next_thread() call NM_kill_this_buffer() if line('.') != line('$') norm j call NM_search_show_thread() else echo 'No more messages.' endif endfunction function! s:NM_show_archive_thread() call NM_tag('', ['-inbox']) call NM_show_next_thread() endfunction function! s:NM_show_mark_read_then_archive_thread() call NM_tag('', ['-unread', '-inbox']) call NM_show_next_thread() endfunction function! s:NM_show_mark_read_then_next_open_message() echo 'not implemented' endfunction function! s:NM_show_previous_message() echo 'not implemented' endfunction "XXX pythonise function! s:NM_show_reply() let cmd = ['reply'] call add(cmd, 'id:' . NM_show_message_id()) let data = NM_run(cmd) let lines = split(data, "\n") call NM_newComposeBuffer(lines, 0) endfunction function! s:NM_show_view_all_mime_parts() echo 'not implemented' endfunction "Show the raw message for current line in a new buffer function! s:NM_show_view_raw_message() exec printf('python nm_vim.vim_get_object(%d, 0)', line('.')) if !exists('obj') return endif call NM_create_buffer('rawmessage') python nm_vim.RawMessage(vim.eval("obj['id']")) call NM_set_map('n', g:nm_vimpy_raw_message_nmaps) endfunction function! s:NM_show_add_tag() echo 'not implemented' endfunction function! s:NM_show_remove_tag() echo 'not implemented' endfunction function! s:NM_show_advance() let advance_tags = ['-unread'] exec printf('python nm_vim.vim_get_object(%d, 0)', line('.')) if !exists('obj') return endif call NM_tag(['id:' . obj['id']], advance_tags) if obj['end'] == line('$') call NM_kill_this_buffer() else call NM_jump_message(1) endif endfunction function! s:NM_show_pipe_message() echo 'not implemented' endfunction function! s:NM_show_view_attachment() exec printf('python nm_vim.vim_view_attachment(%d)', line('.')) endfunction function! s:NM_show_save_attachment() let filename = input('Where to save the attachment: ', './', 'file') if len(filename) try python nm_vim.vim_save_attachment(int(vim.eval('line(".")')), vim.eval('filename')) echo 'Attachment saved successfully.' endtry endif endfunction " --- --- show screen helper functions {{{2 function! s:NM_show_message_id() exec printf('python nm_vim.vim_get_object(%d, 0)', line('.')) if exists('obj') return obj['id'] else return '' endfunction " --- implement compose screen {{{1 function! s:NM_cmd_compose(words, body_lines) let lines = [] let start_on_line = 0 let hdrs = { } if !has_key(hdrs, 'From') || !len(hdrs['From']) let me = NM_compose_get_user_email() let hdrs['From'] = [ me ] endif for key in g:nm_vimpy_compose_headers let text = has_key(hdrs, key) ? join(hdrs[key], ', ') : '' call add(lines, key . ': ' . text) if !start_on_line && !strlen(text) let start_on_line = len(lines) endif endfor for [key,val] in items(hdrs) if match(g:nm_vimpy_compose_headers, key) == -1 let line = key . ': ' . join(val, ', ') call add(lines, line) endif endfor call add(lines, '') if !start_on_line let start_on_line = len(lines) + 1 endif call extend(lines, [ '', '' ]) call NM_newComposeBuffer(lines, start_on_line) endfunction function! s:NM_compose_send() let fname = expand('%') try python nm_vim.get_current_buffer().send() call NM_kill_this_buffer() call delete(fname) echo 'Mail sent successfully.' endtry endfunction function! s:NM_compose_attach() let attachment = input('Enter attachment filename: ', '', 'file') if len(attachment) python nm_vim.get_current_buffer().attach(vim.eval('attachment')) endif endfunction function! s:NM_compose_next_entry_area() let lnum = line('.') let hdr_end = NM_compose_find_line_match(1,'^$',1) if lnum < hdr_end let lnum = lnum + 1 let line = getline(lnum) if match(line, '^\([^:]\+\):\s*$') == -1 call cursor(lnum, strlen(line) + 1) return '' endif while match(getline(lnum+1), '^\s') != -1 let lnum = lnum + 1 endwhile call cursor(lnum, strlen(getline(lnum)) + 1) return '' elseif lnum == hdr_end call cursor(lnum+1, strlen(getline(lnum+1)) + 1) return '' endif if mode() == 'i' if !getbufvar(bufnr('.'), '&et') return "\t" endif let space = '' let shiftwidth = a:shiftwidth let shiftwidth = shiftwidth - ((virtcol('.')-1) % shiftwidth) " we assume no one has shiftwidth set to more than 40 :) return ' '[0:shiftwidth] endif endfunction " --- --- compose screen helper functions {{{2 function! s:NM_compose_get_user_email() " TODO: do this properly (still), i.e., allow for multiple email accounts let email = substitute(system('notmuch config get user.primary_email'), '\v(^\s*|\s*$|\n)', '', 'g') return email endfunction function! s:NM_compose_find_line_match(start, pattern, failure) let lnum = a:start let lend = line('$') while lnum < lend if match(getline(lnum), a:pattern) != -1 return lnum endif let lnum = lnum + 1 endwhile return a:failure endfunction " --- notmuch helper functions {{{1 function! s:NM_create_buffer(type) let prev_bufnr = bufnr('%') enew setlocal buftype=nofile setlocal bufhidden=hide execute printf('set filetype=nm_vimpy-%s', a:type) execute printf('set syntax=nm_vimpy-%s', a:type) "XXX this should probably go let b:nm_prev_bufnr = prev_bufnr endfunction "set some options for "menu"-like buffers -- folders/searches function! s:NM_finalize_menu_buffer() setlocal nomodifiable setlocal cursorline setlocal nowrap endfunction function! s:NM_newBuffer(how, type, content) if strlen(a:how) exec a:how else enew endif setlocal buftype=nofile readonly modifiable scrolloff=0 sidescrolloff=0 silent put=a:content keepjumps 0d setlocal nomodifiable execute printf('set filetype=notmuch-%s', a:type) execute printf('set syntax=notmuch-%s', a:type) endfunction function! s:NM_newFileBuffer(fdir, fname, type, lines) let fdir = expand(a:fdir) if !isdirectory(fdir) call mkdir(fdir, 'p') endif let file_name = NM_mktemp(fdir, a:fname) if writefile(a:lines, file_name) throw 'Eeek! couldn''t write to temporary file ' . file_name endif exec printf('edit %s', file_name) setlocal buftype= noreadonly modifiable scrolloff=0 sidescrolloff=0 execute printf('set filetype=notmuch-%s', a:type) execute printf('set syntax=notmuch-%s', a:type) endfunction function! s:NM_newComposeBuffer(lines, start_on_line) let lines = a:lines let start_on_line = a:start_on_line let real_hdr_start = 1 if g:notmuch_compose_header_help let help_lines = [ \ 'Notmuch-Help: Type in your message here; to help you use these bindings:', \ 'Notmuch-Help: ,a - attach a file', \ 'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)', \ 'Notmuch-Help: ,q - abort the message', \ 'Notmuch-Help: - skip through header lines', \ ] call extend(lines, help_lines, 0) let real_hdr_start = len(help_lines) if start_on_line > 0 let start_on_line = start_on_line + len(help_lines) endif endif if exists('g:nm_vimpy_signature') call extend(lines, ['', '-- ']) call extend(lines, g:nm_vimpy_signature) endif let prev_bufnr = bufnr('%') call NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mail', \ 'compose', lines) let b:nm_prev_bufnr = prev_bufnr call NM_set_map('n', g:nm_vimpy_compose_nmaps) if start_on_line > 0 && start_on_line <= len(lines) call cursor(start_on_line, strlen(getline(start_on_line)) + 1) else call cursor(real_hdr_start, strlen(getline(real_hdr_start)) + 1) call NM_compose_next_entry_area() endif if g:notmuch_compose_insert_mode_start startinsert! endif python nm_vim.Compose() endfunction function! s:NM_mktemp(dir, name) let time_stamp = strftime('%Y%m%d-%H%M%S') let file_name = substitute(a:dir,'/*$','/','') . printf(a:name, time_stamp) " TODO: check if it exists, try again return file_name endfunction function! s:NM_shell_escape(word) " TODO: use shellescape() let word = substitute(a:word, '''', '\\''', 'g') return '''' . word . '''' endfunction function! s:NM_run(args) let words = a:args call map(words, 's:NM_shell_escape(v:val)') let cmd = g:notmuch_cmd . ' ' . join(words) . '< /dev/null' let out = system(cmd) let err = v:shell_error if err echohl Error echo substitute(out, '\n*$', '', '') echohl None return '' else return out endif endfunction " --- external mail handling helpers {{{1 function! s:NM_new_mail() call NM_cmd_compose([], []) endfunction " --- tag manipulation helpers {{{1 " used to combine an array of words with prefixes and separators " example: " NM_combine_tags('tag:', ['one', 'two', 'three'], 'OR', '()') " -> ['(', 'tag:one', 'OR', 'tag:two', 'OR', 'tag:three', ')'] function! s:NM_combine_tags(word_prefix, words, separator, brackets) let res = [] for word in a:words if len(res) && strlen(a:separator) call add(res, a:separator) endif call add(res, a:word_prefix . word) endfor if len(res) > 1 && strlen(a:brackets) if strlen(a:brackets) != 2 throw 'Eeek! brackets arg to NM_combine_tags must be 2 chars' endif call insert(res, a:brackets[0]) call add(res, a:brackets[1]) endif return res endfunction " --- other helpers {{{1 function! s:NM_kill_this_buffer() let prev_bufnr = b:nm_prev_bufnr python nm_vim.delete_current_buffer() bdelete! exec printf("buffer %d", prev_bufnr) endfunction function! s:NM_search_expand(arg) let word = expand(a:arg) let prev_bufnr = bufnr('%') call NM_cmd_search(word, 0) let b:nm_prev_bufnr = prev_bufnr endfunction function! s:NM_tag(filter, tags) let filter = len(a:filter) ? a:filter : [NM_search_thread_id()] if !len(filter) throw 'Eeek! I couldn''t find the thead id!' endif python nm_vim.get_current_buffer().tag(tags = vim.eval("a:tags"), querystr = vim.eval('join(filter)')) endfunction " --- process and set the defaults {{{1 function! s:NM_set_defaults(force) setlocal bufhidden=hide for [key, dflt] in items(s:nm_vimpy_defaults) let cmd = '' if !a:force && exists(key) && type(dflt) == type(eval(key)) continue elseif type(dflt) == type(0) let cmd = printf('let %s = %d', key, dflt) elseif type(dflt) == type('') let cmd = printf('let %s = ''%s''', key, dflt) " FIXME: not sure why this didn't work when dflt is an array "elseif type(dflt) == type([]) " let cmd = printf('let %s = %s', key, string(dflt)) else echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,%s]', \ a:force, key, string(dflt)) continue endif exec cmd endfor endfunction call NM_set_defaults(0) " for some reason NM_set_defaults() didn't work for arrays... if !exists('g:nm_vimpy_folders') let g:nm_vimpy_folders = s:nm_vimpy_folders_defaults endif if !exists('g:nm_vimpy_show_headers') let g:nm_vimpy_show_headers = s:nm_vimpy_show_headers_defaults endif if !exists('g:nm_vimpy_signature') if filereadable(glob('~/.signature')) let g:nm_vimpy_signature = readfile(glob('~/.signature')) endif endif if !exists('g:nm_vimpy_compose_headers') let g:nm_vimpy_compose_headers = s:nm_vimpy_compose_headers_defaults endif " --- assign keymaps {{{1 function! s:NM_set_map(type, maps) for [key, code] in items(a:maps) exec printf('%snoremap %s %s', a:type, key, code) endfor endfunction " --- command handler {{{1 function! NMVimpy() call NM_cmd_folders(g:nm_vimpy_folders) endfunction "Custom foldtext() for show buffers, which indents folds to "represent thread structure function! NM_show_foldtext() if v:foldlevel != 1 return foldtext() endif let indentlevel = matchstr(getline(v:foldstart), '^[0-9]\+') return repeat(' ', indentlevel) . getline(v:foldstart + 1) endfunction "Completion of search prompt function! NM_search_type_completion(arg_lead, cmd_line, cursor_pos) let keywords = 'from:' . "\n" . \ 'to:' . "\n" . \ 'subject:' . "\n" . \ 'attachment:' . "\n" . \ 'tag:' . "\n" . \ 'id:' . "\n" . \ 'thread:' . "\n" . \ 'folder:' . "\n" . \ 'and' . "\n" . \ 'or' let s_idx = strridx(a:arg_lead, " ") let col_idx = stridx(a:arg_lead, ":", s_idx) let prefix = strpart(a:arg_lead, 0, s_idx + 1) if col_idx < 0 return prefix . substitute(keywords, "\n", "\n" . prefix, "g") endif if stridx(a:arg_lead, 'tag:', s_idx) >= 0 python nm_vim.vim_get_tags() return prefix . 'tag:' . substitute(taglist, "\n", "\n" . prefix . 'tag:', "g") endif return '' endfunction function! NM_tag_name_completion(arg_lead, cmd_line, cursor_pos) let s_idx = strridx(a:arg_lead, " ") let prefix = strpart(a:arg_lead, 0, s_idx + 1) python nm_vim.vim_get_tags() return prefix . substitute(taglist, "\n", "\n" . prefix, "g") endfunction let s:notmuch_loaded = 1