1 Return-Path: <anton@khirnov.net>
\r
2 X-Original-To: notmuch@notmuchmail.org
\r
3 Delivered-To: notmuch@notmuchmail.org
\r
4 Received: from localhost (localhost [127.0.0.1])
\r
5 by olra.theworths.org (Postfix) with ESMTP id 8D147431FD0
\r
6 for <notmuch@notmuchmail.org>; Sun, 15 May 2011 12:21:07 -0700 (PDT)
\r
7 X-Virus-Scanned: Debian amavisd-new at olra.theworths.org
\r
11 X-Spam-Status: No, score=1.845 tagged_above=-999 required=5
\r
12 tests=[LONGWORDS=1.844, WEIRD_QUOTING=0.001] autolearn=disabled
\r
13 Received: from olra.theworths.org ([127.0.0.1])
\r
14 by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)
\r
15 with ESMTP id lo3fDz2gKhhp for <notmuch@notmuchmail.org>;
\r
16 Sun, 15 May 2011 12:21:03 -0700 (PDT)
\r
17 X-Greylist: delayed 323 seconds by postgrey-1.32 at olra;
\r
18 Sun, 15 May 2011 12:21:03 PDT
\r
19 Received: from lain.khirnov.net (lain.khirnov.net [193.85.154.54])
\r
20 by olra.theworths.org (Postfix) with ESMTP id 19A0F431FB6
\r
21 for <notmuch@notmuchmail.org>; Sun, 15 May 2011 12:21:03 -0700 (PDT)
\r
22 Received: from localhost (localhost [127.0.0.1])
\r
23 by lain.khirnov.net (Postfix) with ESMTP id 01412C127B
\r
24 for <notmuch@notmuchmail.org>; Sun, 15 May 2011 21:15:39 +0200 (CEST)
\r
25 Received: from lain.khirnov.net ([127.0.0.1])
\r
26 by localhost (mail.khirnov.net [127.0.0.1]) (amavisd-new, port 10024)
\r
27 with ESMTP id 5IEjlamK6tts for <notmuch@notmuchmail.org>;
\r
28 Sun, 15 May 2011 21:15:32 +0200 (CEST)
\r
29 Received: from zohar.localdomain (unknown [192.168.0.1])
\r
30 by lain.khirnov.net (Postfix) with ESMTP id C47E7C0DEF
\r
31 for <notmuch@notmuchmail.org>; Sun, 15 May 2011 21:15:32 +0200 (CEST)
\r
32 Received: from zohar.khirnov.net (localhost [127.0.0.1])
\r
33 by zohar.localdomain (Postfix) with ESMTP id 97FCC7F43E
\r
34 for <notmuch@notmuchmail.org>; Sun, 15 May 2011 21:15:32 +0200 (CEST)
\r
35 Content-Type: multipart/mixed; boundary="===============0474093823=="
\r
37 User-Agent: Notmuch-vim EXPERIMENTAL
\r
38 Message-ID: <20110515191531.25709.21869@zohar.khirnov.net>
\r
39 Date: Sun, 15 May 2011 21:15:31 +0200
\r
40 To: notmuch@notmuchmail.org
\r
41 From: anton@khirnov.net
\r
42 Subject: [RFC/PATCH] Vim client rewrite
\r
43 X-BeenThere: notmuch@notmuchmail.org
\r
44 X-Mailman-Version: 2.1.13
\r
46 List-Id: "Use and development of the notmuch mail system."
\r
47 <notmuch.notmuchmail.org>
\r
48 List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,
\r
49 <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>
\r
50 List-Archive: <http://notmuchmail.org/pipermail/notmuch>
\r
51 List-Post: <mailto:notmuch@notmuchmail.org>
\r
52 List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>
\r
53 List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,
\r
54 <mailto:notmuch-request@notmuchmail.org?subject=subscribe>
\r
55 X-List-Received-Date: Sun, 15 May 2011 19:21:07 -0000
\r
57 --===============0474093823==
\r
58 Content-Type: text/plain; charset="utf-8"
\r
60 Content-Transfer-Encoding: quoted-printable
\r
65 my attempts to make the vim client more usable somehow spiraled out of
\r
66 control and turned into a huge rewrite. The intermediate results I
\r
67 hereby present for your amusement and comments.
\r
68 (attached as whole files, since the patch would be unreadable)
\r
70 The main point of the rewrite is splitting of a large part of the code
\r
71 into Python. This should have the following advantages:
\r
72 1) python-notmuch bindings can be used, which should allow for cleaner
\r
73 and more reliable code than running the binary and parsing its output
\r
75 (also provides a nice use case for python-notmuch)
\r
76 2) Python's huge standard library makes implementing some features MUCH eas=
\r
78 3) More people know Python than vimscript, thus making the client
\r
81 The code is =CE=B1 quality, but should be close to usable.
\r
82 It already has some features not present in the mainline vim client,
\r
83 like attachments (viewing and sending, saving to file should be trivial
\r
84 to add, will be done when I have some time), better support for unicode
\r
87 Some UI features from the mainline versions that I didn't use were
\r
88 removed and customization options are somewhat lacking atm. This is of
\r
89 course to be improved later, depending on the responses.
\r
91 Comments, bugreports and fixes very much welcome.
\r
95 --===============0474093823==
\r
96 Content-Type: text/plain; charset="utf-8"
\r
98 Content-Transfer-Encoding: quoted-printable
\r
99 Content-Disposition: attachment; filename="notmuch.vim"
\r
101 " notmuch.vim plugin --- run notmuch within vim
\r
103 " Copyright =C2=A9 Carl Worth
\r
105 " This file is part of Notmuch.
\r
107 " Notmuch is free software: you can redistribute it and/or modify it
\r
108 " under the terms of the GNU General Public License as published by
\r
109 " the Free Software Foundation, either version 3 of the License, or
\r
110 " (at your option) any later version.
\r
112 " Notmuch is distributed in the hope that it will be useful, but
\r
113 " WITHOUT ANY WARRANTY; without even the implied warranty of
\r
114 " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
\r
115 " General Public License for more details.
\r
117 " You should have received a copy of the GNU General Public License
\r
118 " along with Notmuch. If not, see <http://www.gnu.org/licenses/>.
\r
120 " Authors: Bart Trojanowski <bart@jukie.net>
\r
121 " Contributors: Felipe Contreras <felipe.contreras@gmail.com>,
\r
122 " Peter Hartman <peterjohnhartman@gmail.com>
\r
125 if exists('s:notmuch_loaded') || &cp
\r
128 let s:notmuch_loaded =3D 1
\r
131 " --- configuration defaults {{{1
\r
133 let s:notmuch_defaults =3D {
\r
134 \ 'g:notmuch_cmd': 'notmuch' =
\r
137 \ 'g:notmuch_search_newest_first': 1 =
\r
140 \ 'g:notmuch_compose_insert_mode_start': 1 =
\r
142 \ 'g:notmuch_compose_header_help': 1 =
\r
144 \ 'g:notmuch_compose_temp_file_dir': '~/.notmuch/compose/' =
\r
146 \ 'g:notmuch_fcc_maildir': 'sent' =
\r
150 " defaults for g:notmuch_folders
\r
151 " override with: let g:notmuch_folders =3D [ ... ]
\r
152 let s:notmuch_folders_defaults =3D [
\r
153 \ [ 'new', 'tag:inbox and tag:unread' ],
\r
154 \ [ 'inbox', 'tag:inbox' ],
\r
155 \ [ 'unread', 'tag:unread' ],
\r
158 let s:notmuch_show_headers_defaults =3D [
\r
168 " defaults for g:notmuch_compose_headers
\r
169 " override with: let g:notmuch_compose_headers =3D [ ... ]
\r
170 let s:notmuch_compose_headers_defaults =3D [
\r
178 " --- keyboard mapping definitions {{{1
\r
180 " --- --- bindings for folders mode {{{2
\r
182 let g:notmuch_folders_maps =3D {
\r
183 \ 'm': ':call <SID>NM_new_mail()<CR>',
\r
184 \ 's': ':call <SID>NM_search_prompt(0)<CR>',
\r
185 \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
\r
186 \ '=3D': ':call <SID>NM_folders_refresh_view()<CR>',
\r
187 \ '<Enter>': ':call <SID>NM_folders_show_search('''')<CR>',
\r
188 \ '<Space>': ':call <SID>NM_folders_show_search(''tag:unread'')<=
\r
190 \ 'tt': ':call <SID>NM_folders_from_tags()<CR>',
\r
193 " --- --- bindings for search screen {{{2
\r
194 let g:notmuch_search_maps =3D {
\r
195 \ '<Space>': ':call <SID>NM_search_show_thread()<CR>',
\r
196 \ '<Enter>': ':call <SID>NM_search_show_thread()<CR>',
\r
197 \ '<C-]>': ':call <SID>NM_search_expand(''<cword>'')<CR>',
\r
198 \ 'a': ':call <SID>NM_search_archive_thread()<CR>',
\r
199 \ 'A': ':call <SID>NM_search_mark_read_then_archive_thread=
\r
201 \ 'D': ':call <SID>NM_search_delete_thread()<CR>',
\r
202 \ 'f': ':call <SID>NM_search_filter()<CR>',
\r
203 \ 'm': ':call <SID>NM_new_mail()<CR>',
\r
204 \ 'o': ':call <SID>NM_search_toggle_order()<CR>',
\r
205 \ 'r': ':call <SID>NM_search_reply_to_thread()<CR>',
\r
206 \ 's': ':call <SID>NM_search_prompt(0)<CR>',
\r
207 \ ',s': ':call <SID>NM_search_prompt(1)<CR>',
\r
208 \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
\r
209 \ '+': ':call <SID>NM_search_add_tags([])<CR>',
\r
210 \ '-': ':call <SID>NM_search_remove_tags([])<CR>',
\r
211 \ '=3D': ':call <SID>NM_search_refresh_view()<CR>',
\r
214 " --- --- bindings for show screen {{{2
\r
215 let g:notmuch_show_maps =3D {
\r
216 \ '<C-P>': ':call <SID>NM_jump_message(-1)<CR>',
\r
217 \ '<C-N>': ':call <SID>NM_jump_message(+1)<CR>',
\r
218 \ '<C-]>': ':call <SID>NM_search_expand(''<cword>'')<CR>',
\r
219 \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
\r
220 \ 's': ':call <SID>NM_search_prompt(0)<CR>',
\r
223 \ 'a': ':call <SID>NM_show_archive_thread()<CR>',
\r
224 \ 'A': ':call <SID>NM_show_mark_read_then_archive_thread()=
\r
226 \ 'N': ':call <SID>NM_show_mark_read_then_next_open_messag=
\r
228 \ 'v': ':call <SID>NM_show_view_all_mime_parts()<CR>',
\r
229 \ '+': ':call <SID>NM_show_add_tag()<CR>',
\r
230 \ '-': ':call <SID>NM_show_remove_tag()<CR>',
\r
231 \ '<Space>': ':call <SID>NM_show_advance()<CR>',
\r
232 \ '\|': ':call <SID>NM_show_pipe_message()<CR>',
\r
234 \ '<S-Tab>': ':call <SID>NM_show_previous_fold()<CR>',
\r
235 \ '<Tab>': ':call <SID>NM_show_next_fold()<CR>',
\r
236 \ '<Enter>': ':call <SID>NM_show_view_attachment()<CR>',
\r
238 \ 'r': ':call <SID>NM_show_reply()<CR>',
\r
239 \ 'm': ':call <SID>NM_new_mail()<CR>',
\r
242 " --- --- bindings for compose screen {{{2
\r
243 let g:notmuch_compose_nmaps =3D {
\r
244 \ ',s': ':call <SID>NM_compose_send()<CR>',
\r
245 \ ',a': ':call <SID>NM_compose_attach()<CR>',
\r
246 \ ',q': ':call <SID>NM_kill_this_buffer()<CR>',
\r
247 \ '<Tab>': ':call <SID>NM_compose_next_entry_area()<CR>',
\r
249 let g:notmuch_compose_imaps =3D {
\r
250 \ '<Tab>': '<C-r>=3D<SID>NM_compose_next_entry_area()<CR>',
\r
253 " --- implement folders screen {{{1
\r
255 " Create the folders buffer.
\r
256 " Takes a list of [ folder name, query string]
\r
257 " TODO decorate (help on the first line?)
\r
258 function! s:NM_cmd_folders(folders)
\r
259 call <SID>NM_create_buffer('folders')
\r
260 silent 0put!=3D' Notmuch plugin.'
\r
261 python nm_vim.SavedSearches(vim.eval("a:folders"))
\r
262 call <SID>NM_finalize_menu_buffer()
\r
263 call <SID>NM_set_map('n', g:notmuch_folders_maps)
\r
266 " Show a folder for each existing tag.
\r
267 function! s:NM_folders_from_tags()
\r
269 python nm_vim.vim_get_tags()
\r
270 for tag in split(taglist, '\n')
\r
271 call add(folders, [tag, 'tag:' . tag ])
\r
274 call <SID>NM_cmd_folders(folders)
\r
277 " --- --- folders screen action functions {{{2
\r
279 " Refresh the folders screen
\r
280 function! s:NM_folders_refresh_view()
\r
281 let lno =3D line('.')
\r
282 setlocal modifiable
\r
284 python nm_vim.get_current_buffer().refresh()
\r
285 setlocal nomodifiable
\r
286 exec printf('norm %dG', lno)
\r
289 " Show contents of the folder corresponding to current line AND query
\r
290 function! s:NM_folders_show_search(query)
\r
291 exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
\r
294 let querystr =3D '(' . obj['id'] . ') and ' . a:query
\r
296 let querystr =3D obj['id']
\r
299 call <SID>NM_cmd_search(querystr, 0)
\r
303 " Create the search buffer corresponding to querystr.
\r
304 " If relative is 1, the search is relative to current buffer
\r
305 function! s:NM_cmd_search(querystr, relative)
\r
306 let cur_buf =3D bufnr('%')
\r
307 call <SID>NM_create_buffer('search')
\r
309 exec printf('python nm_vim.Search(querystr =3D "%s", parent =3D nm_=
\r
310 vim.nm_buffers["%d"])', a:querystr, cur_buf)
\r
312 exec printf('python nm_vim.Search(querystr =3D "%s")', a:querystr)
\r
314 call <SID>NM_finalize_menu_buffer()
\r
315 call <SID>NM_set_map('n', g:notmuch_search_maps)
\r
318 " --- --- search screen action functions {{{2
\r
320 " Show the thread corresponding to current line
\r
321 function! s:NM_search_show_thread()
\r
322 let querystr =3D <SID>NM_search_thread_id()
\r
324 call <SID>NM_cmd_show(querystr)
\r
328 " Search according to input from user.
\r
329 " If edit is 1, current query string is inserted to prompt for editing.
\r
330 function! s:NM_search_prompt(edit)
\r
332 python nm_vim.vim_get_id()
\r
336 let querystr =3D input('Search: ', buf_id, 'custom,Search_type_completi=
\r
339 call <SID>NM_cmd_search(querystr, 0)
\r
343 " Filter current search, i.e. search for
\r
344 " (current querystr) AND (user input)
\r
345 function! s:NM_search_filter()
\r
346 let querystr =3D input('Filter: ', '', 'custom,Search_type_completion')
\r
348 call <SID>NM_cmd_search(querystr, 1)
\r
352 """"""""""""""""""""""'' TODO
\r
353 function! s:NM_search_archive_thread()
\r
354 call <SID>NM_tag([], ['-inbox'])
\r
358 function! s:NM_search_mark_read_then_archive_thread()
\r
359 call <SID>NM_tag([], ['-unread', '-inbox'])
\r
363 function! s:NM_search_delete_thread()
\r
364 call <SID>NM_tag([], ['+junk','-inbox','-unread'])
\r
368 """""""""""""""""""""""""""""""""""""""""""""""""""""
\r
370 " XXX This function is broken
\r
371 function! s:NM_search_toggle_order()
\r
372 let g:notmuch_search_newest_first =3D !g:notmuch_search_newest_first
\r
373 " FIXME: maybe this would be better done w/o reading re-reading the=
\r
375 " reversing the b:nm_raw_lines and the buffer lines would b=
\r
377 call <SID>NM_search_refresh_view()
\r
380 "XXX this function is broken
\r
381 function! s:NM_search_reply_to_thread()
\r
382 python vim.command('let querystr =3D "%s"'%nm_vim.get_current_buffer().=
\r
384 let cmd =3D ['reply']
\r
385 call add(cmd, <SID>NM_search_thread_id())
\r
386 call add(cmd, 'AND')
\r
387 call extend(cmd, [querystr])
\r
389 let data =3D <SID>NM_run(cmd)
\r
390 let lines =3D split(data, "\n")
\r
391 call <SID>NM_newComposeBuffer(lines, 0)
\r
394 function! s:NM_search_add_tags(tags)
\r
395 call <SID>NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags)
\r
398 function! s:NM_search_remove_tags(tags)
\r
399 call <SID>NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags)
\r
402 function! s:NM_search_refresh_view()
\r
403 let lno =3D line('.')
\r
404 setlocal modifiable
\r
406 python nm_vim.get_current_buffer().refresh()
\r
407 setlocal nomodifiable
\r
408 " FIXME: should find the line of the thread we were on if possible
\r
409 exec printf('norm %dG', lno)
\r
412 " --- --- search screen helper functions {{{2
\r
414 function! s:NM_search_thread_id()
\r
415 exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
\r
417 return 'thread:' . obj['id']
\r
422 function! s:NM_search_add_remove_tags(prompt, prefix, intags)
\r
423 if type(a:intags) !=3D type([]) || len(a:intags) =3D=3D 0
\r
424 " TODO: input() can support completion
\r
425 let text =3D input(a:prompt)
\r
429 let tags =3D split(text, ' ')
\r
431 let tags =3D a:intags
\r
433 call map(tags, 'a:prefix . v:val')
\r
434 call <SID>NM_tag([], tags)
\r
437 " --- implement show screen {{{1
\r
439 function! s:NM_cmd_show(querystr)
\r
440 "TODO: folding, syntax
\r
441 call <SID>NM_create_buffer('show')
\r
442 exec printf('python nm_vim.ShowThread("%s")', a:querystr)
\r
444 call <SID>NM_set_map('n', g:notmuch_show_maps)
\r
445 setlocal fillchars=3D
\r
446 setlocal foldtext=3DNM_show_foldtext()
\r
447 setlocal foldcolumn=3D6
\r
448 setlocal foldmethod=3Dsyntax
\r
451 function! s:NM_jump_message(offset)
\r
452 "TODO implement can_change_thread and find_matching, nicer positioning
\r
453 exec printf('python nm_vim.vim_get_object(%d, %d)', line('.'), a:offset)
\r
456 exec printf('norm %dGzt', obj['start'])
\r
461 function! s:NM_show_next_thread()
\r
462 call <SID>NM_kill_this_buffer()
\r
463 if line('.') !=3D line('$')
\r
465 call <SID>NM_search_show_thread()
\r
467 echo 'No more messages.'
\r
471 function! s:NM_show_archive_thread()
\r
472 call <SID>NM_tag('', ['-inbox'])
\r
473 call <SID>NM_show_next_thread()
\r
476 function! s:NM_show_mark_read_then_archive_thread()
\r
477 call <SID>NM_tag('', ['-unread', '-inbox'])
\r
478 call <SID>NM_show_next_thread()
\r
481 function! s:NM_show_mark_read_then_next_open_message()
\r
482 echo 'not implemented'
\r
485 function! s:NM_show_previous_message()
\r
486 echo 'not implemented'
\r
490 function! s:NM_show_reply()
\r
491 let cmd =3D ['reply']
\r
492 call add(cmd, 'id:' . <SID>NM_show_message_id())
\r
494 let data =3D <SID>NM_run(cmd)
\r
495 let lines =3D split(data, "\n")
\r
496 call <SID>NM_newComposeBuffer(lines, 0)
\r
499 function! s:NM_show_view_all_mime_parts()
\r
500 echo 'not implemented'
\r
503 function! s:NM_show_view_raw_message()
\r
504 echo 'not implemented'
\r
507 function! s:NM_show_add_tag()
\r
508 echo 'not implemented'
\r
511 function! s:NM_show_remove_tag()
\r
512 echo 'not implemented'
\r
515 function! s:NM_show_advance()
\r
516 let advance_tags =3D ['-unread']
\r
518 exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
\r
523 call <SID>NM_tag(['id:' . obj['id']], advance_tags)
\r
524 if obj['end'] =3D=3D line('$')
\r
525 call <SID>NM_kill_this_buffer()
\r
527 call <SID>NM_jump_message(1)
\r
531 function! s:NM_show_pipe_message()
\r
532 echo 'not implemented'
\r
535 function! s:NM_show_view_attachment()
\r
536 exec printf('python nm_vim.vim_view_attachment(%d)', line('.'))
\r
539 " --- --- show screen helper functions {{{2
\r
541 function! s:NM_show_message_id()
\r
542 exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))
\r
549 " --- implement compose screen {{{1
\r
551 function! s:NM_cmd_compose(words, body_lines)
\r
553 let start_on_line =3D 0
\r
557 if !has_key(hdrs, 'From') || !len(hdrs['From'])
\r
558 let me =3D <SID>NM_compose_get_user_email()
\r
559 let hdrs['From'] =3D [ me ]
\r
562 for key in g:notmuch_compose_headers
\r
563 let text =3D has_key(hdrs, key) ? join(hdrs[key], ', ') : ''
\r
564 call add(lines, key . ': ' . text)
\r
565 if !start_on_line && !strlen(text)
\r
566 let start_on_line =3D len(lines)
\r
570 for [key,val] in items(hdrs)
\r
571 if match(g:notmuch_compose_headers, key) =3D=3D -1
\r
572 let line =3D key . ': ' . join(val, ', ')
\r
573 call add(lines, line)
\r
577 call add(lines, '')
\r
579 let start_on_line =3D len(lines) + 1
\r
582 call extend(lines, [ '', '' ])
\r
584 call <SID>NM_newComposeBuffer(lines, start_on_line)
\r
587 function! s:NM_compose_send()
\r
588 let fname =3D expand('%')
\r
591 python nm_vim.get_current_buffer().send()
\r
592 call <SID>NM_kill_this_buffer()
\r
595 echo 'Mail sent successfully.'
\r
599 function! s:NM_compose_attach()
\r
600 let attachment =3D input('Enter attachment filename: ', '', 'file')
\r
602 exec printf('python nm_vim.get_current_buffer().attach("%s")', atta=
\r
607 function! s:NM_compose_next_entry_area()
\r
608 let lnum =3D line('.')
\r
609 let hdr_end =3D <SID>NM_compose_find_line_match(1,'^$',1)
\r
611 let lnum =3D lnum + 1
\r
612 let line =3D getline(lnum)
\r
613 if match(line, '^\([^:]\+\):\s*$') =3D=3D -1
\r
614 call cursor(lnum, strlen(line) + 1)
\r
617 while match(getline(lnum+1), '^\s') !=3D -1
\r
618 let lnum =3D lnum + 1
\r
620 call cursor(lnum, strlen(getline(lnum)) + 1)
\r
623 elseif lnum =3D=3D hdr_end
\r
624 call cursor(lnum+1, strlen(getline(lnum+1)) + 1)
\r
627 if mode() =3D=3D 'i'
\r
628 if !getbufvar(bufnr('.'), '&et')
\r
632 let shiftwidth =3D a:shiftwidth
\r
633 let shiftwidth =3D shiftwidth - ((virtcol('.')-1) % shiftwidth)
\r
634 " we assume no one has shiftwidth set to more than 40 :)
\r
635 return ' '[0:shiftwi=
\r
640 " --- --- compose screen helper functions {{{2
\r
642 function! s:NM_compose_get_user_email()
\r
643 " TODO: do this properly (still), i.e., allow for multiple email ac=
\r
645 let email =3D substitute(system('notmuch config get user.primary_em=
\r
646 ail'), '\v(^\s*|\s*$|\n)', '', 'g')
\r
650 function! s:NM_compose_find_line_match(start, pattern, failure)
\r
651 let lnum =3D a:start
\r
652 let lend =3D line('$')
\r
654 if match(getline(lnum), a:pattern) !=3D -1
\r
657 let lnum =3D lnum + 1
\r
663 " --- notmuch helper functions {{{1
\r
664 function! s:NM_create_buffer(type)
\r
665 let prev_bufnr =3D bufnr('%')
\r
668 setlocal buftype=3Dnofile
\r
669 execute printf('set filetype=3Dnotmuch-%s', a:type)
\r
670 execute printf('set syntax=3Dnotmuch-%s', a:type)
\r
671 "XXX this should probably go
\r
672 let b:nm_prev_bufnr =3D prev_bufnr
\r
675 "set some options for "menu"-like buffers -- folders/searches
\r
676 function! s:NM_finalize_menu_buffer()
\r
677 setlocal nomodifiable
\r
678 setlocal cursorline
\r
682 function! s:NM_newBuffer(how, type, content)
\r
688 setlocal buftype=3Dnofile readonly modifiable scrolloff=3D0 sidescr=
\r
690 silent put=3Da:content
\r
692 setlocal nomodifiable
\r
693 execute printf('set filetype=3Dnotmuch-%s', a:type)
\r
694 execute printf('set syntax=3Dnotmuch-%s', a:type)
\r
697 function! s:NM_newFileBuffer(fdir, fname, type, lines)
\r
698 let fdir =3D expand(a:fdir)
\r
699 if !isdirectory(fdir)
\r
700 call mkdir(fdir, 'p')
\r
702 let file_name =3D <SID>NM_mktemp(fdir, a:fname)
\r
703 if writefile(a:lines, file_name)
\r
704 throw 'Eeek! couldn''t write to temporary file ' . file_name
\r
706 exec printf('edit %s', file_name)
\r
707 setlocal buftype=3D noreadonly modifiable scrolloff=3D0 sidescrollo=
\r
709 execute printf('set filetype=3Dnotmuch-%s', a:type)
\r
710 execute printf('set syntax=3Dnotmuch-%s', a:type)
\r
713 function! s:NM_newComposeBuffer(lines, start_on_line)
\r
714 let lines =3D a:lines
\r
715 let start_on_line =3D a:start_on_line
\r
716 let real_hdr_start =3D 1
\r
717 if g:notmuch_compose_header_help
\r
718 let help_lines =3D [
\r
719 \ 'Notmuch-Help: Type in your message here; to help you u=
\r
720 se these bindings:',
\r
721 \ 'Notmuch-Help: ,a - attach a file',
\r
722 \ 'Notmuch-Help: ,s - send the message (Notmuch-Help=
\r
723 lines will be removed)',
\r
724 \ 'Notmuch-Help: ,q - abort the message',
\r
725 \ 'Notmuch-Help: <Tab> - skip through header lines',
\r
727 call extend(lines, help_lines, 0)
\r
728 let real_hdr_start =3D len(help_lines)
\r
729 if start_on_line > 0
\r
730 let start_on_line =3D start_on_line + len(help_line=
\r
734 if exists('g:notmuch_signature')
\r
735 call extend(lines, ['', '--'])
\r
736 call extend(lines, g:notmuch_signature)
\r
740 let prev_bufnr =3D bufnr('%')
\r
741 call <SID>NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mai=
\r
743 \ 'compose', lines)
\r
744 let b:nm_prev_bufnr =3D prev_bufnr
\r
746 call <SID>NM_set_map('n', g:notmuch_compose_nmaps)
\r
747 call <SID>NM_set_map('i', g:notmuch_compose_imaps)
\r
749 if start_on_line > 0 && start_on_line <=3D len(lines)
\r
750 call cursor(start_on_line, strlen(getline(start_on_line)) +=
\r
753 call cursor(real_hdr_start, strlen(getline(real_hdr_start))=
\r
755 call <SID>NM_compose_next_entry_area()
\r
758 if g:notmuch_compose_insert_mode_start
\r
762 python nm_vim.Compose()
\r
765 function! s:NM_mktemp(dir, name)
\r
766 let time_stamp =3D strftime('%Y%m%d-%H%M%S')
\r
767 let file_name =3D substitute(a:dir,'/*$','/','') . printf(a:name, t=
\r
769 " TODO: check if it exists, try again
\r
773 function! s:NM_shell_escape(word)
\r
774 " TODO: use shellescape()
\r
775 let word =3D substitute(a:word, '''', '\\''', 'g')
\r
776 return '''' . word . ''''
\r
779 function! s:NM_run(args)
\r
780 let words =3D a:args
\r
781 call map(words, 's:NM_shell_escape(v:val)')
\r
782 let cmd =3D g:notmuch_cmd . ' ' . join(words) . '< /dev/null'
\r
784 let out =3D system(cmd)
\r
785 let err =3D v:shell_error
\r
789 echo substitute(out, '\n*$', '', '')
\r
797 " --- external mail handling helpers {{{1
\r
799 function! s:NM_new_mail()
\r
800 call <SID>NM_cmd_compose([], [])
\r
803 " --- tag manipulation helpers {{{1
\r
805 " used to combine an array of words with prefixes and separators
\r
807 " NM_combine_tags('tag:', ['one', 'two', 'three'], 'OR', '()')
\r
808 " -> ['(', 'tag:one', 'OR', 'tag:two', 'OR', 'tag:three', ')']
\r
809 function! s:NM_combine_tags(word_prefix, words, separator, brackets)
\r
811 for word in a:words
\r
812 if len(res) && strlen(a:separator)
\r
813 call add(res, a:separator)
\r
815 call add(res, a:word_prefix . word)
\r
817 if len(res) > 1 && strlen(a:brackets)
\r
818 if strlen(a:brackets) !=3D 2
\r
819 throw 'Eeek! brackets arg to NM_combine_tags must b=
\r
822 call insert(res, a:brackets[0])
\r
823 call add(res, a:brackets[1])
\r
828 " --- other helpers {{{1
\r
830 function! s:NM_kill_this_buffer()
\r
831 let prev_bufnr =3D b:nm_prev_bufnr
\r
832 python nm_vim.delete_current_buffer()
\r
834 exec printf("buffer %d", prev_bufnr)
\r
837 function! s:NM_search_expand(arg)
\r
838 let word =3D expand(a:arg)
\r
839 let prev_bufnr =3D bufnr('%')
\r
840 call <SID>NM_cmd_search(word, 0)
\r
841 let b:nm_prev_bufnr =3D prev_bufnr
\r
844 function! s:NM_tag(filter, tags)
\r
845 let filter =3D len(a:filter) ? a:filter : [<SID>NM_search_thread_id()]
\r
847 throw 'Eeek! I couldn''t find the thead id!'
\r
849 exec printf('python nm_vim.get_current_buffer().tag(tags =3D vim.eval("=
\r
850 a:tags"), querystr =3D "%s")', join(filter))
\r
853 " --- process and set the defaults {{{1
\r
855 function! NM_set_defaults(force)
\r
856 setlocal bufhidden=3Dhide
\r
857 for [key, dflt] in items(s:notmuch_defaults)
\r
859 if !a:force && exists(key) && type(dflt) =3D=3D type(eval(key))
\r
861 elseif type(dflt) =3D=3D type(0)
\r
862 let cmd =3D printf('let %s =3D %d', key, dflt)
\r
863 elseif type(dflt) =3D=3D type('')
\r
864 let cmd =3D printf('let %s =3D ''%s''', key, dflt)
\r
865 " FIXME: not sure why this didn't work when dflt is an array
\r
866 "elseif type(dflt) =3D=3D type([])
\r
867 " let cmd =3D printf('let %s =3D %s', key, string(dflt))
\r
869 echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,=
\r
871 \ a:force, key, string(dflt))
\r
877 call NM_set_defaults(0)
\r
879 " for some reason NM_set_defaults() didn't work for arrays...
\r
880 if !exists('g:notmuch_folders')
\r
881 let g:notmuch_folders =3D s:notmuch_folders_defaults
\r
884 if !exists('g:notmuch_show_headers')
\r
885 let g:notmuch_show_headers =3D s:notmuch_show_headers_defaults
\r
888 if !exists('g:notmuch_signature')
\r
889 if filereadable(glob('~/.signature'))
\r
890 let g:notmuch_signature =3D readfile(glob('~/.signature'))
\r
893 if !exists('g:notmuch_compose_headers')
\r
894 let g:notmuch_compose_headers =3D s:notmuch_compose_headers_defaults
\r
897 " --- assign keymaps {{{1
\r
899 function! s:NM_set_map(type, maps)
\r
900 for [key, code] in items(a:maps)
\r
901 exec printf('%snoremap <buffer> %s %s', a:type, key, code)
\r
905 " --- command handler {{{1
\r
907 function! NotMuch()
\r
908 if !exists('s:notmuch_inited')
\r
909 " init the python layer
\r
911 exec "python sys.path +=3D [r'" . s:python_path . "']"
\r
912 python import vim, nm_vim
\r
914 let s:notmuch_inited =3D 1
\r
917 call <SID>NM_cmd_folders(g:notmuch_folders)
\r
920 "Custom foldtext() for show buffers, which indents folds to
\r
921 "represent thread structure
\r
922 function! NM_show_foldtext()
\r
923 if v:foldlevel !=3D 1
\r
926 let numlines =3D v:foldend - v:foldstart + 1
\r
927 let indentlevel =3D matchstr(getline(v:foldstart), '^[0-9]\+')
\r
928 return repeat(' ', indentlevel) . getline(v:foldstart + 1)
\r
931 "Completion of search prompt
\r
932 "TODO properly deal with complex queries
\r
933 function! Search_type_completion(arg_lead, cmd_line, cursor_pos)
\r
934 let idx =3D stridx(a:arg_lead, ':')
\r
936 return 'from:' . "\n" .
\r
938 \ 'subject:' . "\n" .
\r
939 \ 'attachment:' . "\n" .
\r
942 \ 'thread:' . "\n" .
\r
945 if stridx(a:arg_lead, 'tag:') >=3D 0
\r
946 python nm_vim.vim_get_tags()
\r
947 return 'tag:' . substitute(taglist, "\n", "\ntag:", "g")
\r
954 command! NotMuch call NotMuch()
\r
955 let s:python_path =3D expand('<sfile>:p:h')
\r
957 --===============0474093823==
\r
958 Content-Type: text/x-python; charset="utf-8"
\r
960 Content-Transfer-Encoding: quoted-printable
\r
961 Content-Disposition: attachment; filename="nm_vim.py"
\r
965 # This file is part of Notmuch.
\r
967 # Notmuch is free software: you can redistribute it and/or modify it
\r
968 # under the terms of the GNU General Public License as published by
\r
969 # the Free Software Foundation, either version 3 of the License, or
\r
970 # (at your option) any later version.
\r
972 # Notmuch is distributed in the hope that it will be useful, but
\r
973 # WITHOUT ANY WARRANTY; without even the implied warranty of
\r
974 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
\r
975 # General Public License for more details.
\r
977 # You should have received a copy of the GNU General Public License
\r
978 # along with Notmuch. If not, see <http://www.gnu.org/licenses/>.
\r
981 # python-notmuch wrapper for the notmuch vim client
\r
984 import email, email.header, email.charset, email.utils, email.message
\r
985 import email.mime.text, email.mime.audio, email.mime.image, email.mime.mult=
\r
1000 class NMBuffer(object):
\r
1002 An object mapping line ranges in current buffer to notmuch structures.
\r
1005 # a string identifying the buffer
\r
1008 # a list of NMBufferElement subclasses representing objects in current =
\r
1012 def __new__(cls, *args, **kwargs):
\r
1013 bufnr =3D vim.eval('bufnr("%")')
\r
1014 if bufnr in nm_buffers:
\r
1015 return nm_buffers[bufnr]
\r
1016 ret =3D object.__new__(cls)
\r
1017 nm_buffers[bufnr] =3D ret
\r
1020 def __init__(self):
\r
1021 self.objects =3D []
\r
1023 def get_object(self, line, offset):
\r
1025 Get an object that's offset objects away from given line or None.
\r
1026 E.g. offset =3D 0 gets the object on the line, offset =3D 1 gets
\r
1027 next, offset =3D -1 previous.
\r
1029 if not self.objects or line < self.objects[0].start or line > self.=
\r
1033 for i in xrange(len(self.objects)):
\r
1034 obj =3D self.objects[i]
\r
1035 if line >=3D obj.start and line <=3D obj.end:
\r
1036 return self.objects[ max(min(len(self.objects) - 1, i + off=
\r
1039 def tag(self, tags, querystr =3D None):
\r
1041 querystr =3D '( %s ) and ( %s )'%(self.id, querystr)
\r
1043 querystr =3D self.id
\r
1045 db =3D notmuch.Database(mode =3D notmuch.Database.MODE.READ_WRITE)
\r
1046 map_query(db, querystr, lambda m, l: self._tag_message(m, tags))
\r
1048 def _tag_message(self, message, tags):
\r
1050 if tag[0] =3D=3D '+':
\r
1051 message.add_tag(tag[1:])
\r
1052 elif tag[0] =3D=3D '-':
\r
1053 message.remove_tag(tag[1:])
\r
1055 class SavedSearches(NMBuffer):
\r
1057 This buffer displays a list of saved searches ('folders').
\r
1060 def __init__(self, folders):
\r
1062 @param folders A list of (folder name, query string) tuples.
\r
1064 super(SavedSearches, self).__init__()
\r
1065 for folder in folders:
\r
1066 self.objects.append(Folder(0, 0, folder[1], folder[0]))
\r
1069 def refresh(self):
\r
1070 b =3D vim.current.buffer
\r
1071 db =3D notmuch.Database()
\r
1072 for obj in self.objects:
\r
1073 q =3D db.create_query(obj.id)
\r
1074 q1 =3D db.create_query('( %s ) and tag:unread'%(obj.id))
\r
1075 b.append('{0:>7} {1:7} {2: <30s} ({3})'.format(q.count_messages=
\r
1076 (), '({0})'.format(q1.count_messages()),
\r
1079 obj.start =3D obj.end =3D len(b) - 1
\r
1081 def __repr__(self):
\r
1082 return '<Vim-notmuch saved searches buffer.>'
\r
1085 raise TypeError('Attempted to tag in folders view.')
\r
1087 class Search(NMBuffer):
\r
1089 This buffer displays results of a db search -- a list of found threads.
\r
1092 def __init__(self, querystr =3D '', parent =3D None):
\r
1093 super(Search, self).__init__()
\r
1095 assert(querystr or parent)
\r
1099 self.id =3D parent.id
\r
1102 self.id =3D '( %s ) and ( %s )'%(self.id, querystr)
\r
1104 self.id =3D querystr
\r
1108 def refresh(self):
\r
1109 b =3D vim.current.buffer
\r
1110 self.objects =3D []
\r
1111 db =3D notmuch.Database()
\r
1112 q =3D db.create_query(self.id)
\r
1113 q.set_sort(q.SORT.NEWEST_FIRST) # FIXME allow different search =
\r
1115 for t in q.search_threads():
\r
1117 # we have to decode the unicode strings to get the alignment ri=
\r
1119 datestr =3D get_relative_date(t.get_newest_date())
\r
1120 authors =3D t.get_authors().decode('utf-8')
\r
1121 subj =3D t.get_subject().decode('utf-8')
\r
1122 tags =3D str(t.get_tags()).decode('utf-8')
\r
1123 b.append((u'%-12s %3s/%3s %-20.20s | %s (%s)'%(datestr, t.get_m=
\r
1124 atched_messages(), t.get_total_messages(),
\r
1126 ags)).encode('utf-8'))
\r
1127 self.objects.append(NMBufferElement(start, len(b) - 1, t.get_th=
\r
1130 def __repr__(self):
\r
1131 return '<Vim-notmuch search buffer: %s>'%(self.id)
\r
1133 class ShowThread(NMBuffer):
\r
1135 This buffer represents a thread view.
\r
1138 # a list of temporary files for viewing attachments
\r
1139 # they will be automagically closed and deleted on this object's demise
\r
1140 _tmpfiles =3D None
\r
1142 # a list of headers to show
\r
1145 def __init__(self, querystr):
\r
1146 self.id =3D querystr
\r
1147 self._tmpfiles =3D []
\r
1150 def refresh(self):
\r
1151 self._headers =3D vim.eval('g:notmuch_show_headers')
\r
1152 self.objects =3D []
\r
1153 db =3D notmuch.Database()
\r
1154 map_query(db, self.id, self._print_message)
\r
1156 def view_attachment(self, line):
\r
1158 View attachment corresponding to given line.
\r
1160 message =3D self.get_object(line, 0)
\r
1162 print 'No message on this line.'
\r
1166 for a in message.attachments:
\r
1167 if line =3D=3D a.start:
\r
1173 f =3D tempfile.NamedTemporaryFile()
\r
1174 f.write(data.get_payload(decode =3D True))
\r
1176 os.fsync(f.fileno())
\r
1178 caps =3D mailcap.getcaps()
\r
1179 ret =3D mailcap.findmatch(caps, data.get_content_type(), filename =
\r
1182 with open(os.devnull, 'w') as null:
\r
1183 subprocess.Popen(shlex.split(ret[0]), stderr =3D null, stdo=
\r
1185 self._tmpfiles.append(f)
\r
1187 def _read_text_payload(self, part):
\r
1189 Try converting the payload of the MIME part into utf-8.
\r
1191 p =3D part.get_payload(decode =3D True)
\r
1192 ch =3D part.get_content_charset('utf-8')
\r
1193 if ch !=3D 'utf-8':
\r
1195 p =3D p.decode(ch).encode('utf-8')
\r
1196 except LookupError:
\r
1197 # if the encoding is unknown, try utf-8
\r
1200 except UnicodeDecodeError:
\r
1201 return 'Unknown encoding: %s, cannot decode message'%ch
\r
1204 def _print_part(self, part):
\r
1206 Walk through the part recursively and print it
\r
1207 and its subparts to current buffer.
\r
1209 if part.is_multipart():
\r
1210 if part.get_content_subtype() =3D=3D 'alternative':
\r
1211 # try to find the plaintext version, if that fails just try=
\r
1212 to print the first
\r
1213 for subpart in part.get_payload():
\r
1214 if subpart.get_content_type() =3D=3D 'text/plain':
\r
1215 return self._print_part(subpart)
\r
1216 self._print_part(part.get_payload()[0])
\r
1218 for subpart in part.get_payload():
\r
1219 self._print_part(subpart)
\r
1221 b =3D vim.current.buffer
\r
1222 if part.get('Content-Disposition', '').lower().startswith('atta=
\r
1224 self.objects[-1].attachments.append(Attachment(len(b), len(=
\r
1226 b.append(('[ Attachment: %s (%s)]'%(part.get_filename(), pa=
\r
1227 rt.get_content_type())).split('\n'))
\r
1229 if part.get_content_maintype() =3D=3D 'text':
\r
1230 p =3D self._read_text_payload(part)
\r
1231 b.append(p.split('\n'))
\r
1233 def _print_message(self, message, level):
\r
1234 b =3D vim.current.buffer
\r
1235 msg =3D Message(len(b), 0, message.get_message_id())
\r
1236 self.objects.append(msg)
\r
1238 fp =3D open(message.get_filename())
\r
1239 email_msg =3D email.message_from_file(fp)
\r
1243 b.append('%d/'%level + 20*'-' + 'message start' + 20*'-' + '\\')
\r
1244 b.append('%s: %s (%s) (%s)'%(get_author(message.get_header('from'))=
\r
1245 , message.get_header('subject'),
\r
1246 get_relative_date(message.get_date()), ' '=
\r
1247 .join(message.get_tags())))
\r
1249 # print the headers
\r
1250 # TODO toggle all (like in mutt)
\r
1251 for header in self._headers:
\r
1252 if header in email_msg:
\r
1253 b.append(('%s: %s'%(header, decode_header(email_msg[header]=
\r
1257 self._print_part(email_msg)
\r
1259 b.append('\\' + 20*'-' + 'message end' + 20*'-' + '/')
\r
1260 msg.end =3D len(b) - 1
\r
1262 def __repr__(self):
\r
1263 return '<Vim-notmuch thread buffer: %s.>'%self.id
\r
1265 class Compose(NMBuffer):
\r
1267 _attachment_prefix =3D 'Notmuch-Attachment: '
\r
1269 def __init__(self):
\r
1270 super(Compose, self).__init__()
\r
1271 # python wants to use base64 for some reason, force quoted-printable
\r
1272 email.charset.add_charset('utf-8', email.charset.QP, email.charset.=
\r
1275 def _encode_header(self, text):
\r
1277 text.decode('ascii')
\r
1279 except UnicodeDecodeError:
\r
1280 return email.header.Header(text, 'utf-8').encode()
\r
1282 def attach(self, filename):
\r
1283 type, encoding =3D mimetypes.guess_type(filename)
\r
1284 if encoding or not type:
\r
1285 type =3D 'application/octet-stream'
\r
1286 vim.current.buffer.append('%s%s:%s'%(self._attachment_prefix, filen=
\r
1291 Send the message in current buffer.
\r
1293 b =3D vim.current.buffer
\r
1296 # parse attachments
\r
1297 attachments =3D []
\r
1298 while b[i].startswith(self._attachment_prefix):
\r
1299 filename, sep, type =3D b[i][len(self._attachment_prefix):].rpa=
\r
1301 attachments.append((os.path.expanduser(filename), type))
\r
1303 # skip the inline help
\r
1304 while b[i].startswith('Notmuch-Help:'):
\r
1310 from_addr =3D None
\r
1314 key, sep, val =3D b[i].partition(':')
\r
1318 key.decode('ascii')
\r
1319 except UnicodeDecodeError:
\r
1320 raise ValueError('Header name must be ASCII only.')
\r
1322 if not val.strip():
\r
1323 # skip empty headers
\r
1326 if key.lower() in ('to', 'cc', 'bcc'):
\r
1327 names, addrs =3D zip(*email.utils.getaddresses([val]))
\r
1328 names =3D map(self._encode_header, names)
\r
1330 recipients +=3D addrs
\r
1331 if key.lower() =3D=3D 'bcc':
\r
1333 val =3D ','.join(map(email.utils.formataddr, zip(names, add=
\r
1336 if key.lower() =3D=3D 'from':
\r
1337 from_addr =3D email.utils.parseaddr(val)[1]
\r
1338 val =3D self._encode_header(val)
\r
1340 headers[key] =3D val
\r
1342 body =3D email.mime.text.MIMEText('\n'.join(b[i:]), 'plain', 'utf-8=
\r
1345 if not attachments:
\r
1348 msg =3D email.mime.multipart.MIMEMultipart()
\r
1350 for attachment in attachments:
\r
1351 maintype, subtype =3D attachment[1].split('/', 1)
\r
1353 if maintype =3D=3D 'text':
\r
1354 with open(attachment[0]) as f:
\r
1355 part =3D email.mime.text.MIMEText(f.read(), subtype=
\r
1358 if maintype =3D=3D 'image':
\r
1359 obj =3D email.mime.image.MIMEImage
\r
1360 elif maintype =3D=3D 'audio':
\r
1361 obj =3D email.mime.audio.MIMEAudio
\r
1363 obj =3D email.mime.application.MIMEApplication
\r
1365 with open(attachment[0]) as f:
\r
1366 part =3D obj(f.read(), subtype)
\r
1368 part.add_header('Content-Disposition', 'attachment',
\r
1369 filename =3D os.path.basename(attachment[0]=
\r
1373 msg['User-Agent'] =3D 'Notmuch-vim EXPERIMENTAL'
\r
1374 msg['Message-ID'] =3D email.utils.make_msgid()
\r
1375 msg['Date'] =3D email.utils.formatdate(localtime =3D True)
\r
1376 for key in headers:
\r
1377 msg[key] =3D headers[key]
\r
1381 # XXX notmuch-python should export the user email address
\r
1382 raise ValueError('No sender address specified.')
\r
1383 if not recipients:
\r
1384 raise ValueError('No recipient specified.')
\r
1387 fcc =3D vim.eval('g:notmuch_fcc_maildir')
\r
1389 dbroot =3D notmuch.Database().get_path()
\r
1390 mdir =3D mailbox.Maildir(os.path.join(dbroot, fcc))
\r
1393 # TODO configurable host
\r
1394 s =3D smtplib.SMTP('localhost')
\r
1395 ret =3D s.sendmail(from_addr, recipients, msg.as_string())
\r
1397 print 'Error sending mail to %s: %s'%(key, ret[key])
\r
1400 class NMBufferElement(object):
\r
1402 This object represents a structure (e.g. folder, thread or message)
\r
1403 corresponding to a range of lines in NMBuffer.
\r
1405 # start and end lines, 0-based
\r
1408 # a string identifying the element
\r
1411 def __init__(self, start, end, id):
\r
1412 self.start =3D start
\r
1416 class Folder(NMBufferElement):
\r
1418 A saved search / 'folder'.
\r
1423 def __init__(self, start, end, id, name):
\r
1424 super(Folder, self).__init__(start, end, id)
\r
1425 self.name =3D name
\r
1427 class Message(NMBufferElement):
\r
1429 attachments =3D None
\r
1431 def __init__(self, start, end, id):
\r
1432 super(Message, self).__init__(start, end, id)
\r
1433 self.attachments =3D []
\r
1435 class Attachment(NMBufferElement):
\r
1438 def __init__(self, start, end, data):
\r
1439 super(Attachment, self).__init__(start, end, '')
\r
1440 self. data =3D data
\r
1442 #### global variables ####
\r
1443 # this dictionary stores the Python objects corresponding to notmuch-managed
\r
1444 # buffers, each indexed by the buffer number
\r
1447 #### utility functions ####
\r
1449 def get_current_buffer():
\r
1451 Get the NMBuffer object associated with current buffer or None.
\r
1454 return nm_buffers[vim.eval('bufnr("%")')]
\r
1458 def delete_current_buffer():
\r
1460 Delete the NMBuffer associated with current buffer.
\r
1462 del nm_buffers[vim.eval('bufnr("%")')]
\r
1464 def get_relative_date(timestamp):
\r
1466 Format a nice representation of 'time' relative to the current time.
\r
1470 5 mins. ago (For times less than 60 minutes ago)
\r
1471 Today 12:30 (For times >60 minutes but still today)
\r
1473 Mon. 12:30 (Before yesterday but fewer than 7 days ago)
\r
1474 October 12 (Between 7 and 180 days ago (about 6 months))
\r
1475 2008-06-30 (More than 180 days ago)
\r
1477 Shamelessly lifted from notmuch-time
\r
1478 TODO: this should probably be in python-notmuch
\r
1481 now =3D datetime.datetime.now()
\r
1482 then =3D datetime.datetime.fromtimestamp(timestamp)
\r
1483 except ValueError:
\r
1488 return "the future"
\r
1490 delta =3D now - then
\r
1492 if delta.days > 180:
\r
1493 return then.strftime("%F") # 2008-06-30
\r
1495 total_seconds =3D delta.seconds + delta.days * 24 * 3600
\r
1496 if total_seconds < 3600:
\r
1497 return "%d min. ago"%(total_seconds / 60)
\r
1499 if delta.days < 7:
\r
1500 if then.day =3D=3D now.day:
\r
1501 return then.strftime("Today %R") # Today 12:30
\r
1502 if then.day + 1 =3D=3D now.day:
\r
1503 return then.strftime("Yest. %R") # Yest. 12:30
\r
1504 return then.strftime("%a. %R") # Mon. 12:30
\r
1506 return then.strftime("%B %d") # October 12
\r
1508 def map_query(db, query, run):
\r
1510 Execute runnable run on every message found for the specified query.
\r
1512 def walk_query(message, run, level):
\r
1513 run(message, level)
\r
1515 replies =3D message.get_replies()
\r
1516 if replies is not None:
\r
1517 for reply in replies:
\r
1518 walk_query(reply, run, level)
\r
1520 q =3D db.create_query(query)
\r
1521 for t in q.search_threads():
\r
1522 for m in t.get_toplevel_messages():
\r
1523 walk_query(m, run, 0)
\r
1525 def decode_header(header):
\r
1527 Decode a RFC 2822 header into a utf-8 string.
\r
1530 for part in email.header.decode_header(header):
\r
1532 ret.append(part[0].decode(part[1]).encode('utf-8'))
\r
1534 ret.append(part[0])
\r
1535 return ''.join(ret)
\r
1537 def get_author(address):
\r
1539 Extract the author name from an address if possible.
\r
1541 #XXX should be in the bindings
\r
1542 if address.endswith('>'):
\r
1544 ret =3D address[:address.rindex('<') - 1].strip()
\r
1547 except ValueError:
\r
1551 #### functions for exporting stuff to viml ####
\r
1553 def vim_get_tags():
\r
1555 Export a string listing all tags in the database, one per line into
\r
1556 a vim variable named 'taglist'.
\r
1558 db =3D notmuch.Database()
\r
1559 tags =3D '\n'.join(db.get_all_tags())
\r
1560 vim.command('let taglist =3D \'%s\''%tags)
\r
1562 def vim_get_object(line, offset):
\r
1564 Export start/end lines and id of the object that's offset objects
\r
1565 aways from given line into a dict named 'obj' in viml.
\r
1566 E.g. offset =3D 0 gets the object on the line, offset =3D 1 gets
\r
1567 next, offset =3D -1 previous.
\r
1568 This method is a noop if current line doesn't correspond to anything.
\r
1570 # vim lines are 1-based
\r
1572 assert line >=3D 0
\r
1574 obj =3D get_current_buffer().get_object(line, offset)
\r
1576 vim.command('let obj =3D { "start" : %d, "end" : %d, "id" : "%s" }'=
\r
1578 obj.start + 1, obj.end + 1, obj.id))
\r
1582 Export the id string of current buffer into a vim string named 'buf_id'.
\r
1584 id =3D get_current_buffer().id
\r
1585 vim.command('let buf_id =3D "%s"'%(id if id else ''))
\r
1587 def vim_view_attachment(line):
\r
1589 View an attachment corresponding to the given vim line
\r
1590 in an external viewer.
\r
1592 line -=3D 1 # vim lines are 1-based
\r
1593 get_current_buffer().view_attachment(line)
\r
1595 --===============0474093823==
\r
1596 Content-Type: text/plain; charset="utf-8"
\r
1598 Content-Transfer-Encoding: quoted-printable
\r
1599 Content-Disposition: attachment; filename="notmuch-folders.vim"
\r
1601 " notmuch folders mode syntax file
\r
1603 syntax region nmFolfers start=3D/^/ end=3D/$/ oneline c=
\r
1604 ontains=3DnmFoldersMessageCount
\r
1605 syntax match nmFoldersMessageCount /^ *[0-9]\+ */ contained nex=
\r
1606 tgroup=3DnmFoldersUnreadCount
\r
1607 syntax match nmFoldersUnreadCount /(.\{-}) */ contained nex=
\r
1608 tgroup=3DnmFoldersName
\r
1609 syntax match nmFoldersName /.*\ze(/ contained nex=
\r
1610 tgroup=3DnmFoldersSearch
\r
1611 syntax match nmFoldersSearch /([^()]\+)$/
\r
1613 highlight link nmFoldersMessageCount Statement
\r
1614 highlight link nmFoldersUnreadCount Underlined
\r
1615 highlight link nmFoldersName Type
\r
1616 highlight link nmFoldersSearch String
\r
1618 highlight CursorLine term=3Dreverse cterm=3Dreverse gui=3Dreverse
\r
1621 --===============0474093823==
\r
1622 Content-Type: text/plain; charset="utf-8"
\r
1624 Content-Transfer-Encoding: quoted-printable
\r
1625 Content-Disposition: attachment; filename="notmuch-search.vim"
\r
1627 syntax region nmSearch start=3D/^/ end=3D/$/ oneline contain=
\r
1628 s=3DnmSearchDate keepend
\r
1629 syntax match nmSearchDate /^.\{-13}/ contained nextgroup=
\r
1630 =3DnmSearchNum skipwhite
\r
1631 syntax match nmSearchNum "[0-9]\+\/" contained nextgroup=
\r
1632 =3DnmSearchTotal skipwhite
\r
1633 syntax match nmSearchTotal /[0-9]\+/ contained nextgroup=
\r
1634 =3DnmSearchFrom skipwhite
\r
1635 syntax match nmSearchFrom /.\{-}\ze|/ contained nextgroup=
\r
1636 =3DnmSearchSubject skipwhite
\r
1637 "XXX this fails on some messages with multiple authors
\r
1638 syntax match nmSearchSubject /.*\ze(/ contained nextgroup=
\r
1640 syntax match nmSearchTags /.\+$/ contained
\r
1642 syntax match nmUnread /^.*(.*\<unread\>.*)$/
\r
1644 highlight link nmSearchDate Statement
\r
1645 highlight link nmSearchNum Number
\r
1646 highlight link nmSearchTotal Type
\r
1647 highlight link nmSearchFrom Include
\r
1648 highlight link nmSearchSubject Normal
\r
1649 highlight link nmSearchTags String
\r
1651 highlight link nmUnread Underlined
\r
1653 --===============0474093823==
\r
1654 Content-Type: text/plain; charset="utf-8"
\r
1656 Content-Transfer-Encoding: quoted-printable
\r
1657 Content-Disposition: attachment; filename="notmuch-show.vim"
\r
1659 " notmuch show mode syntax file
\r
1661 setlocal conceallevel=3D2
\r
1662 setlocal concealcursor=3Dvinc
\r
1664 syntax region nmMessage matchgroup=3DIgnore concealends start=3D'[0-9]=
\r
1665 \+\/-*message start-*\\' end=3D'\\-*message end-*\/' fold contains=3D@nmSho=
\r
1668 "TODO what about those
\r
1669 syntax cluster nmShowMsgDesc contains=3DnmShowMsgDescWho,nmShowMsgDescDate,=
\r
1671 syntax match nmShowMsgDescWho /[^)]\+)/ contained
\r
1672 syntax match nmShowMsgDescDate / ([^)]\+[0-9]) / contained
\r
1673 syntax match nmShowMsgDescTags /([^)]\+)$/ contained
\r
1675 syntax cluster nmShowMsgBody contains=3D@nmShowMsgBodyMail,@nmShowMsgBodyGit
\r
1676 syntax include @nmShowMsgBodyMail syntax/mail.vim
\r
1677 silent! syntax include @nmShowMsgBodyGit syntax/notmuch-git-diff.vim
\r
1679 highlight nmShowMsgDescWho term=3Dreverse cterm=3Dreverse gui=3Dreverse
\r
1680 highlight link nmShowMsgDescDate Type
\r
1681 highlight link nmShowMsgDescTags String
\r
1683 "TODO what about this?
\r
1684 highlight Folded term=3Dreverse ctermfg=3DLightGrey ctermbg=3DBlack guifg=
\r
1685 =3DLightGray guibg=3DBlack
\r
1687 --===============0474093823==--
\r