Re: [PATCH] emacs: wash: make word-wrap bound message width
[notmuch-archives.git] / 55 / 2cbeb2213b5bc5d186744fa499704bf38cc666
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
8 X-Spam-Flag: NO\r
9 X-Spam-Score: 1.845\r
10 X-Spam-Level: *\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
36 MIME-Version: 1.0\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
45 Precedence: list\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
56 \r
57 --===============0474093823==\r
58 Content-Type: text/plain; charset="utf-8"\r
59 MIME-Version: 1.0\r
60 Content-Transfer-Encoding: quoted-printable\r
61 \r
62 \r
63 Hi,\r
64 \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
69 \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
74    with regexps.\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
77 ier.\r
78 3) More people know Python than vimscript, thus making the client\r
79    development easier\r
80 \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
85 and more.\r
86 \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
90 \r
91 Comments, bugreports and fixes very much welcome.\r
92 \r
93 --\r
94 Anton Khirnov\r
95 --===============0474093823==\r
96 Content-Type: text/plain; charset="utf-8"\r
97 MIME-Version: 1.0\r
98 Content-Transfer-Encoding: quoted-printable\r
99 Content-Disposition: attachment; filename="notmuch.vim"\r
100 \r
101 " notmuch.vim plugin --- run notmuch within vim\r
102 "\r
103 " Copyright =C2=A9 Carl Worth\r
104 "\r
105 " This file is part of Notmuch.\r
106 "\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
111 "\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
116 "\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
119 "\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
123 "\r
124 \r
125 if exists('s:notmuch_loaded') || &cp\r
126     finish\r
127 endif\r
128 let s:notmuch_loaded =3D 1\r
129 \r
130 \r
131 " --- configuration defaults {{{1\r
132 \r
133 let s:notmuch_defaults =3D {\r
134         \ 'g:notmuch_cmd':                           'notmuch'             =\r
135        ,\r
136         \\r
137         \ 'g:notmuch_search_newest_first':           1                     =\r
138        ,\r
139         \\r
140         \ 'g:notmuch_compose_insert_mode_start':     1                     =\r
141        ,\r
142         \ 'g:notmuch_compose_header_help':           1                     =\r
143        ,\r
144         \ 'g:notmuch_compose_temp_file_dir':         '~/.notmuch/compose/' =\r
145        ,\r
146         \ 'g:notmuch_fcc_maildir':                   'sent'                =\r
147        ,\r
148         \ }\r
149 \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
156         \ ]\r
157 \r
158 let s:notmuch_show_headers_defaults =3D [\r
159     \ 'From',\r
160     \ 'To',\r
161     \ 'Cc',\r
162     \ 'Subject',\r
163     \ 'Date',\r
164     \ 'Reply-To',\r
165     \ 'Message-Id',\r
166     \]\r
167 \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
171         \ 'From',\r
172         \ 'To',\r
173         \ 'Cc',\r
174         \ 'Bcc',\r
175         \ 'Subject'\r
176         \ ]\r
177 \r
178 " --- keyboard mapping definitions {{{1\r
179 \r
180 " --- --- bindings for folders mode {{{2\r
181 \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
189 CR>',\r
190         \ 'tt':         ':call <SID>NM_folders_from_tags()<CR>',\r
191         \ }\r
192 \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
200 ()<CR>',\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
212         \ }\r
213 \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
221         \\r
222         \\r
223         \ 'a':          ':call <SID>NM_show_archive_thread()<CR>',\r
224         \ 'A':          ':call <SID>NM_show_mark_read_then_archive_thread()=\r
225 <CR>',\r
226         \ 'N':          ':call <SID>NM_show_mark_read_then_next_open_messag=\r
227 e()<CR>',\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
233         \\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
237         \\r
238         \ 'r':          ':call <SID>NM_show_reply()<CR>',\r
239         \ 'm':          ':call <SID>NM_new_mail()<CR>',\r
240         \ }\r
241 \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
248         \ }\r
249 let g:notmuch_compose_imaps =3D {\r
250         \ '<Tab>':      '<C-r>=3D<SID>NM_compose_next_entry_area()<CR>',\r
251         \ }\r
252 \r
253 " --- implement folders screen {{{1\r
254 \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
264 endfunction\r
265 \r
266 " Show a folder for each existing tag.\r
267 function! s:NM_folders_from_tags()\r
268     let folders =3D []\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
272     endfor\r
273 \r
274     call <SID>NM_cmd_folders(folders)\r
275 endfunction\r
276 \r
277 " --- --- folders screen action functions {{{2\r
278 \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
283         silent norm 3GdG\r
284         python nm_vim.get_current_buffer().refresh()\r
285         setlocal nomodifiable\r
286         exec printf('norm %dG', lno)\r
287 endfunction\r
288 \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
292     if exists('obj')\r
293         if len(a:query)\r
294             let querystr =3D '(' . obj['id'] . ') and ' . a:query\r
295         else\r
296             let querystr =3D obj['id']\r
297         endif\r
298 \r
299         call <SID>NM_cmd_search(querystr, 0)\r
300     endif\r
301 endfunction\r
302 \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
308     if a:relative\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
311     else\r
312         exec printf('python nm_vim.Search(querystr =3D "%s")', a:querystr)\r
313     endif\r
314     call <SID>NM_finalize_menu_buffer()\r
315     call <SID>NM_set_map('n', g:notmuch_search_maps)\r
316 endfunction\r
317 \r
318 " --- --- search screen action functions {{{2\r
319 \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
323     if len(querystr)\r
324         call <SID>NM_cmd_show(querystr)\r
325     endif\r
326 endfunction\r
327 \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
331     if a:edit\r
332         python nm_vim.vim_get_id()\r
333     else\r
334         let buf_id =3D ''\r
335     endif\r
336     let querystr =3D input('Search: ', buf_id, 'custom,Search_type_completi=\r
337 on')\r
338     if len(querystr)\r
339         call <SID>NM_cmd_search(querystr, 0)\r
340     endif\r
341 endfunction\r
342 \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
347     if len(querystr)\r
348         call <SID>NM_cmd_search(querystr, 1)\r
349     endif\r
350 endfunction\r
351 \r
352 """"""""""""""""""""""'' TODO\r
353 function! s:NM_search_archive_thread()\r
354         call <SID>NM_tag([], ['-inbox'])\r
355         norm j\r
356 endfunction\r
357 \r
358 function! s:NM_search_mark_read_then_archive_thread()\r
359         call <SID>NM_tag([], ['-unread', '-inbox'])\r
360         norm j\r
361 endfunction\r
362 \r
363 function! s:NM_search_delete_thread()\r
364         call <SID>NM_tag([], ['+junk','-inbox','-unread'])\r
365         norm j\r
366 endfunction\r
367 \r
368 """""""""""""""""""""""""""""""""""""""""""""""""""""\r
369 \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
374  lines\r
375         "         reversing the b:nm_raw_lines and the buffer lines would b=\r
376 e better\r
377         call <SID>NM_search_refresh_view()\r
378 endfunction\r
379 \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
383 id)\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
388 \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
392 endfunction\r
393 \r
394 function! s:NM_search_add_tags(tags)\r
395         call <SID>NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags)\r
396 endfunction\r
397 \r
398 function! s:NM_search_remove_tags(tags)\r
399         call <SID>NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags)\r
400 endfunction\r
401 \r
402 function! s:NM_search_refresh_view()\r
403         let lno =3D line('.')\r
404         setlocal modifiable\r
405         norm ggdG\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
410 endfunction\r
411 \r
412 " --- --- search screen helper functions {{{2\r
413 \r
414 function! s:NM_search_thread_id()\r
415     exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))\r
416     if exists('obj')\r
417         return 'thread:' . obj['id']\r
418     endif\r
419     return ''\r
420 endfunction\r
421 \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
426                 if !strlen(text)\r
427                         return\r
428                 endif\r
429                 let tags =3D split(text, ' ')\r
430         else\r
431                 let tags =3D a:intags\r
432         endif\r
433         call map(tags, 'a:prefix . v:val')\r
434         call <SID>NM_tag([], tags)\r
435 endfunction\r
436 \r
437 " --- implement show screen {{{1\r
438 \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
443 \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
449 endfunction\r
450 \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
454     if exists('obj')\r
455         silent norm zc\r
456         exec printf('norm %dGzt', obj['start'])\r
457         silent norm zo\r
458     endif\r
459 endfunction\r
460 \r
461 function! s:NM_show_next_thread()\r
462         call <SID>NM_kill_this_buffer()\r
463         if line('.') !=3D line('$')\r
464                 norm j\r
465                 call <SID>NM_search_show_thread()\r
466         else\r
467                 echo 'No more messages.'\r
468         endif\r
469 endfunction\r
470 \r
471 function! s:NM_show_archive_thread()\r
472         call <SID>NM_tag('', ['-inbox'])\r
473         call <SID>NM_show_next_thread()\r
474 endfunction\r
475 \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
479 endfunction\r
480 \r
481 function! s:NM_show_mark_read_then_next_open_message()\r
482         echo 'not implemented'\r
483 endfunction\r
484 \r
485 function! s:NM_show_previous_message()\r
486         echo 'not implemented'\r
487 endfunction\r
488 \r
489 "XXX pythonise\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
493 \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
497 endfunction\r
498 \r
499 function! s:NM_show_view_all_mime_parts()\r
500         echo 'not implemented'\r
501 endfunction\r
502 \r
503 function! s:NM_show_view_raw_message()\r
504         echo 'not implemented'\r
505 endfunction\r
506 \r
507 function! s:NM_show_add_tag()\r
508         echo 'not implemented'\r
509 endfunction\r
510 \r
511 function! s:NM_show_remove_tag()\r
512         echo 'not implemented'\r
513 endfunction\r
514 \r
515 function! s:NM_show_advance()\r
516     let advance_tags =3D ['-unread']\r
517 \r
518     exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))\r
519     if !exists('obj')\r
520         return\r
521     endif\r
522 \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
526     else\r
527         call <SID>NM_jump_message(1)\r
528     endif\r
529 endfunction\r
530 \r
531 function! s:NM_show_pipe_message()\r
532         echo 'not implemented'\r
533 endfunction\r
534 \r
535 function! s:NM_show_view_attachment()\r
536     exec printf('python nm_vim.vim_view_attachment(%d)', line('.'))\r
537 endfunction\r
538 \r
539 " --- --- show screen helper functions {{{2\r
540 \r
541 function! s:NM_show_message_id()\r
542     exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))\r
543     if exists('obj')\r
544         return obj['id']\r
545     else\r
546         return ''\r
547 endfunction\r
548 \r
549 " --- implement compose screen {{{1\r
550 \r
551 function! s:NM_cmd_compose(words, body_lines)\r
552         let lines =3D []\r
553         let start_on_line =3D 0\r
554 \r
555         let hdrs =3D { }\r
556 \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
560         endif\r
561 \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
567                 endif\r
568         endfor\r
569 \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
574                 endif\r
575         endfor\r
576 \r
577         call add(lines, '')\r
578         if !start_on_line\r
579                 let start_on_line =3D len(lines) + 1\r
580         endif\r
581 \r
582         call extend(lines, [ '', '' ])\r
583 \r
584         call <SID>NM_newComposeBuffer(lines, start_on_line)\r
585 endfunction\r
586 \r
587 function! s:NM_compose_send()\r
588     let fname =3D expand('%')\r
589 \r
590     try\r
591         python nm_vim.get_current_buffer().send()\r
592         call <SID>NM_kill_this_buffer()\r
593 \r
594         call delete(fname)\r
595         echo 'Mail sent successfully.'\r
596     endtry\r
597 endfunction\r
598 \r
599 function! s:NM_compose_attach()\r
600     let attachment =3D input('Enter attachment filename: ', '', 'file')\r
601     if len(attachment)\r
602         exec printf('python nm_vim.get_current_buffer().attach("%s")', atta=\r
603 chment)\r
604     endif\r
605 endfunction\r
606 \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
610         if lnum < hdr_end\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
615                         return ''\r
616                 endif\r
617                 while match(getline(lnum+1), '^\s') !=3D -1\r
618                         let lnum =3D lnum + 1\r
619                 endwhile\r
620                 call cursor(lnum, strlen(getline(lnum)) + 1)\r
621                 return ''\r
622 \r
623         elseif lnum =3D=3D hdr_end\r
624                 call cursor(lnum+1, strlen(getline(lnum+1)) + 1)\r
625                 return ''\r
626         endif\r
627         if mode() =3D=3D 'i'\r
628                 if !getbufvar(bufnr('.'), '&et')\r
629                         return "\t"\r
630                 endif\r
631                 let space =3D ''\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
636 dth]\r
637         endif\r
638 endfunction\r
639 \r
640 " --- --- compose screen helper functions {{{2\r
641 \r
642 function! s:NM_compose_get_user_email()\r
643         " TODO: do this properly (still), i.e., allow for multiple email ac=\r
644 counts\r
645         let email =3D substitute(system('notmuch config get user.primary_em=\r
646 ail'), '\v(^\s*|\s*$|\n)', '', 'g')\r
647         return email\r
648 endfunction\r
649 \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
653         while lnum < lend\r
654                 if match(getline(lnum), a:pattern) !=3D -1\r
655                         return lnum\r
656                 endif\r
657                 let lnum =3D lnum + 1\r
658         endwhile\r
659         return a:failure\r
660 endfunction\r
661 \r
662 \r
663 " --- notmuch helper functions {{{1\r
664 function! s:NM_create_buffer(type)\r
665     let prev_bufnr =3D bufnr('%')\r
666 \r
667     enew\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
673 endfunction\r
674 \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
679     setlocal nowrap\r
680 endfunction\r
681 \r
682 function! s:NM_newBuffer(how, type, content)\r
683         if strlen(a:how)\r
684                 exec a:how\r
685         else\r
686                 enew\r
687         endif\r
688         setlocal buftype=3Dnofile readonly modifiable scrolloff=3D0 sidescr=\r
689 olloff=3D0\r
690         silent put=3Da:content\r
691         keepjumps 0d\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
695 endfunction\r
696 \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
701         endif\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
705         endif\r
706         exec printf('edit %s', file_name)\r
707         setlocal buftype=3D noreadonly modifiable scrolloff=3D0 sidescrollo=\r
708 ff=3D0\r
709         execute printf('set filetype=3Dnotmuch-%s', a:type)\r
710         execute printf('set syntax=3Dnotmuch-%s', a:type)\r
711 endfunction\r
712 \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
726                   \ ]\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
731 s)\r
732                 endif\r
733         endif\r
734         if exists('g:notmuch_signature')\r
735                 call extend(lines, ['', '--'])\r
736                 call extend(lines, g:notmuch_signature)\r
737         endif\r
738 \r
739 \r
740         let prev_bufnr =3D bufnr('%')\r
741         call <SID>NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mai=\r
742 l',\r
743                                   \ 'compose', lines)\r
744         let b:nm_prev_bufnr =3D prev_bufnr\r
745 \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
748 \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
751  1)\r
752         else\r
753                 call cursor(real_hdr_start, strlen(getline(real_hdr_start))=\r
754  + 1)\r
755                 call <SID>NM_compose_next_entry_area()\r
756         endif\r
757 \r
758         if g:notmuch_compose_insert_mode_start\r
759                 startinsert!\r
760         endif\r
761 \r
762         python nm_vim.Compose()\r
763 endfunction\r
764 \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
768 ime_stamp)\r
769         " TODO: check if it exists, try again\r
770         return file_name\r
771 endfunction\r
772 \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
777 endfunction\r
778 \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
783 \r
784     let out =3D system(cmd)\r
785     let err =3D v:shell_error\r
786 \r
787     if err\r
788         echohl Error\r
789         echo substitute(out, '\n*$', '', '')\r
790         echohl None\r
791         return ''\r
792     else\r
793         return out\r
794     endif\r
795 endfunction\r
796 \r
797 " --- external mail handling helpers {{{1\r
798 \r
799 function! s:NM_new_mail()\r
800         call <SID>NM_cmd_compose([], [])\r
801 endfunction\r
802 \r
803 " --- tag manipulation helpers {{{1\r
804 \r
805 " used to combine an array of words with prefixes and separators\r
806 " example:\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
810         let res =3D []\r
811         for word in a:words\r
812                 if len(res) && strlen(a:separator)\r
813                         call add(res, a:separator)\r
814                 endif\r
815                 call add(res, a:word_prefix . word)\r
816         endfor\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
820 e 2 chars'\r
821                 endif\r
822                 call insert(res, a:brackets[0])\r
823                 call add(res, a:brackets[1])\r
824         endif\r
825         return res\r
826 endfunction\r
827 \r
828 " --- other helpers {{{1\r
829 \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
833     bdelete!\r
834     exec printf("buffer %d", prev_bufnr)\r
835 endfunction\r
836 \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
842 endfunction\r
843 \r
844 function! s:NM_tag(filter, tags)\r
845     let filter =3D len(a:filter) ? a:filter : [<SID>NM_search_thread_id()]\r
846     if !len(filter)\r
847         throw 'Eeek! I couldn''t find the thead id!'\r
848     endif\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
851 endfunction\r
852 \r
853 " --- process and set the defaults {{{1\r
854 \r
855 function! NM_set_defaults(force)\r
856     setlocal bufhidden=3Dhide\r
857     for [key, dflt] in items(s:notmuch_defaults)\r
858         let cmd =3D ''\r
859         if !a:force && exists(key) && type(dflt) =3D=3D type(eval(key))\r
860             continue\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
868         else\r
869             echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,=\r
870 %s]',\r
871                         \ a:force, key, string(dflt))\r
872             continue\r
873         endif\r
874         exec cmd\r
875     endfor\r
876 endfunction\r
877 call NM_set_defaults(0)\r
878 \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
882 endif\r
883 \r
884 if !exists('g:notmuch_show_headers')\r
885     let g:notmuch_show_headers =3D s:notmuch_show_headers_defaults\r
886 endif\r
887 \r
888 if !exists('g:notmuch_signature')\r
889         if filereadable(glob('~/.signature'))\r
890             let g:notmuch_signature =3D readfile(glob('~/.signature'))\r
891         endif\r
892 endif\r
893 if !exists('g:notmuch_compose_headers')\r
894         let g:notmuch_compose_headers =3D s:notmuch_compose_headers_defaults\r
895 endif\r
896 \r
897 " --- assign keymaps {{{1\r
898 \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
902         endfor\r
903 endfunction\r
904 \r
905 " --- command handler {{{1\r
906 \r
907 function! NotMuch()\r
908     if !exists('s:notmuch_inited')\r
909         " init the python layer\r
910         python import sys\r
911         exec "python sys.path +=3D [r'" . s:python_path . "']"\r
912         python import vim, nm_vim\r
913 \r
914         let s:notmuch_inited =3D 1\r
915     endif\r
916 \r
917     call <SID>NM_cmd_folders(g:notmuch_folders)\r
918 endfunction\r
919 \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
924         return foldtext()\r
925     endif\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
929 endfunction\r
930 \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
935     if idx < 0\r
936         return 'from:' .       "\n" .\r
937              \ 'to:' .         "\n" .\r
938              \ 'subject:' .    "\n" .\r
939              \ 'attachment:' . "\n" .\r
940              \ 'tag:' .        "\n" .\r
941              \ 'id:' .         "\n" .\r
942              \ 'thread:' .     "\n" .\r
943              \ 'folder:'\r
944     endif\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
948     endif\r
949     return ''\r
950 endfunction\r
951 \r
952 " --- glue {{{1\r
953 \r
954 command! NotMuch call NotMuch()\r
955 let s:python_path =3D expand('<sfile>:p:h')\r
956 \r
957 --===============0474093823==\r
958 Content-Type: text/x-python; charset="utf-8"\r
959 MIME-Version: 1.0\r
960 Content-Transfer-Encoding: quoted-printable\r
961 Content-Disposition: attachment; filename="nm_vim.py"\r
962 \r
963 #!/usr/bin/python\r
964 #\r
965 # This file is part of Notmuch.\r
966 #\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
971 #\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
976 #\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
979 #\r
980 \r
981 # python-notmuch wrapper for the notmuch vim client\r
982 \r
983 import datetime\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
986 ipart\r
987 import mailbox\r
988 import mailcap\r
989 import mimetypes\r
990 import notmuch\r
991 import os, os.path\r
992 import shlex\r
993 import smtplib\r
994 import subprocess\r
995 import tempfile\r
996 import vim\r
997 \r
998 #### classes ####\r
999 \r
1000 class NMBuffer(object):\r
1001     """\r
1002     An object mapping line ranges in current buffer to notmuch structures.\r
1003     """\r
1004 \r
1005     # a string identifying the buffer\r
1006     id =3D None\r
1007 \r
1008     # a list of NMBufferElement subclasses representing objects in current =\r
1009 buffer\r
1010     objects =3D None\r
1011 \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
1018         return ret\r
1019 \r
1020     def __init__(self):\r
1021         self.objects =3D []\r
1022 \r
1023     def get_object(self, line, offset):\r
1024         """\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
1028         """\r
1029         if not self.objects or line < self.objects[0].start or line > self.=\r
1030 objects[-1].end:\r
1031             return\r
1032 \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
1037 set), 0) ]\r
1038 \r
1039     def tag(self, tags, querystr =3D None):\r
1040         if querystr:\r
1041             querystr =3D '( %s ) and ( %s )'%(self.id, querystr)\r
1042         else:\r
1043             querystr =3D self.id\r
1044 \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
1047 \r
1048     def _tag_message(self, message, tags):\r
1049         for tag in 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
1054 \r
1055 class SavedSearches(NMBuffer):\r
1056     """\r
1057     This buffer displays a list of saved searches ('folders').\r
1058     """\r
1059 \r
1060     def __init__(self, folders):\r
1061         """\r
1062         @param folders A list of (folder name, query string) tuples.\r
1063         """\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
1067         self.refresh()\r
1068 \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
1077                                                            obj.name, obj.id=\r
1078 ))\r
1079             obj.start =3D obj.end =3D len(b) - 1\r
1080 \r
1081     def __repr__(self):\r
1082         return '<Vim-notmuch saved searches buffer.>'\r
1083 \r
1084     def tag(self):\r
1085         raise TypeError('Attempted to tag in folders view.')\r
1086 \r
1087 class Search(NMBuffer):\r
1088     """\r
1089     This buffer displays results of a db search -- a list of found threads.\r
1090     """\r
1091 \r
1092     def __init__(self, querystr =3D '', parent =3D None):\r
1093         super(Search, self).__init__()\r
1094 \r
1095         assert(querystr or parent)\r
1096 \r
1097         # FIXME simplify\r
1098         if parent:\r
1099             self.id =3D parent.id\r
1100         if querystr:\r
1101             if self.id:\r
1102                 self.id =3D '( %s ) and ( %s )'%(self.id, querystr)\r
1103             else:\r
1104                 self.id =3D querystr\r
1105 \r
1106         self.refresh()\r
1107 \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
1114 orders\r
1115         for t in q.search_threads():\r
1116             start =3D len(b)\r
1117             # we have to decode the unicode strings to get the alignment ri=\r
1118 ght\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
1125                                                            authors, subj, t=\r
1126 ags)).encode('utf-8'))\r
1127             self.objects.append(NMBufferElement(start, len(b) - 1, t.get_th=\r
1128 read_id()))\r
1129 \r
1130     def __repr__(self):\r
1131         return '<Vim-notmuch search buffer: %s>'%(self.id)\r
1132 \r
1133 class ShowThread(NMBuffer):\r
1134     """\r
1135     This buffer represents a thread view.\r
1136     """\r
1137 \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
1141 \r
1142     # a list of headers to show\r
1143     _headers =3D None\r
1144 \r
1145     def __init__(self, querystr):\r
1146         self.id =3D querystr\r
1147         self._tmpfiles =3D []\r
1148         self.refresh()\r
1149 \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
1155 \r
1156     def view_attachment(self, line):\r
1157         """\r
1158         View attachment corresponding to given line.\r
1159         """\r
1160         message =3D self.get_object(line, 0)\r
1161         if not message:\r
1162             print 'No message on this line.'\r
1163             return\r
1164 \r
1165         data =3D None\r
1166         for a in message.attachments:\r
1167             if line =3D=3D a.start:\r
1168                 data =3D a.data\r
1169                 break\r
1170         if not data:\r
1171             return\r
1172 \r
1173         f =3D tempfile.NamedTemporaryFile()\r
1174         f.write(data.get_payload(decode =3D True))\r
1175         f.flush()\r
1176         os.fsync(f.fileno())\r
1177 \r
1178         caps =3D mailcap.getcaps()\r
1179         ret  =3D mailcap.findmatch(caps, data.get_content_type(), filename =\r
1180 =3D f.name)\r
1181         if ret[0]:\r
1182             with open(os.devnull, 'w') as null:\r
1183                 subprocess.Popen(shlex.split(ret[0]), stderr =3D null, stdo=\r
1184 ut =3D null)\r
1185         self._tmpfiles.append(f)\r
1186 \r
1187     def _read_text_payload(self, part):\r
1188         """\r
1189         Try converting the payload of the MIME part into utf-8.\r
1190         """\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
1194             try:\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
1198                 try:\r
1199                     p.decode('utf-8')\r
1200                 except UnicodeDecodeError:\r
1201                     return 'Unknown encoding: %s, cannot decode message'%ch\r
1202         return p\r
1203 \r
1204     def _print_part(self, part):\r
1205         """\r
1206         Walk through the part recursively and print it\r
1207         and its subparts to current buffer.\r
1208         """\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
1217             else:\r
1218                 for subpart in part.get_payload():\r
1219                     self._print_part(subpart)\r
1220         else:\r
1221             b =3D vim.current.buffer\r
1222             if part.get('Content-Disposition', '').lower().startswith('atta=\r
1223 chment'):\r
1224                 self.objects[-1].attachments.append(Attachment(len(b), len(=\r
1225 b), part))\r
1226                 b.append(('[ Attachment: %s (%s)]'%(part.get_filename(), pa=\r
1227 rt.get_content_type())).split('\n'))\r
1228 \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
1232 \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
1237 \r
1238         fp        =3D open(message.get_filename())\r
1239         email_msg =3D email.message_from_file(fp)\r
1240         fp.close()\r
1241 \r
1242         # print the title\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
1248 \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
1254 ))).split('\n'))\r
1255         b.append('')\r
1256 \r
1257         self._print_part(email_msg)\r
1258 \r
1259         b.append('\\' + 20*'-' + 'message end' + 20*'-' + '/')\r
1260         msg.end =3D len(b) - 1\r
1261 \r
1262     def __repr__(self):\r
1263         return '<Vim-notmuch thread buffer: %s.>'%self.id\r
1264 \r
1265 class Compose(NMBuffer):\r
1266 \r
1267     _attachment_prefix =3D 'Notmuch-Attachment: '\r
1268 \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
1273 QP, 'utf-8')\r
1274 \r
1275     def _encode_header(self, text):\r
1276         try:\r
1277             text.decode('ascii')\r
1278             return text\r
1279         except UnicodeDecodeError:\r
1280             return email.header.Header(text, 'utf-8').encode()\r
1281 \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
1287 ame, type), 0)\r
1288 \r
1289     def send(self):\r
1290         """\r
1291         Send the message in current buffer.\r
1292         """\r
1293         b =3D vim.current.buffer\r
1294         i =3D 0\r
1295 \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
1300 rtition(':')\r
1301             attachments.append((os.path.expanduser(filename), type))\r
1302             i +=3D 1\r
1303         # skip the inline help\r
1304         while b[i].startswith('Notmuch-Help:'):\r
1305             i +=3D 1\r
1306 \r
1307         # add the headers\r
1308         headers    =3D {}\r
1309         recipients =3D []\r
1310         from_addr  =3D None\r
1311         while i < len(b):\r
1312             if not b[i]:\r
1313                 break\r
1314             key, sep, val =3D b[i].partition(':')\r
1315             i +=3D 1\r
1316 \r
1317             try:\r
1318                 key.decode('ascii')\r
1319             except UnicodeDecodeError:\r
1320                 raise ValueError('Header name must be ASCII only.')\r
1321 \r
1322             if not val.strip():\r
1323                 # skip empty headers\r
1324                 continue\r
1325 \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
1329 \r
1330                 recipients +=3D addrs\r
1331                 if key.lower() =3D=3D 'bcc':\r
1332                     continue\r
1333                 val =3D ','.join(map(email.utils.formataddr, zip(names, add=\r
1334 rs)))\r
1335             else:\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
1339 \r
1340             headers[key] =3D val\r
1341 \r
1342         body =3D email.mime.text.MIMEText('\n'.join(b[i:]), 'plain', 'utf-8=\r
1343 ')\r
1344         # add the body\r
1345         if not attachments:\r
1346             msg =3D body\r
1347         else:\r
1348             msg  =3D email.mime.multipart.MIMEMultipart()\r
1349             msg.attach(body)\r
1350             for attachment in attachments:\r
1351                 maintype, subtype =3D attachment[1].split('/', 1)\r
1352 \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
1356 , 'utf-8')\r
1357                 else:\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
1362                     else:\r
1363                         obj =3D email.mime.application.MIMEApplication\r
1364 \r
1365                     with open(attachment[0]) as f:\r
1366                         part =3D obj(f.read(), subtype)\r
1367 \r
1368                 part.add_header('Content-Disposition', 'attachment',\r
1369                                 filename =3D os.path.basename(attachment[0]=\r
1370 ))\r
1371                 msg.attach(part)\r
1372 \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
1378 \r
1379         # sanity checks\r
1380         if not from_addr:\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
1385 \r
1386         # send\r
1387         fcc =3D vim.eval('g:notmuch_fcc_maildir')\r
1388         if fcc:\r
1389             dbroot =3D notmuch.Database().get_path()\r
1390             mdir   =3D mailbox.Maildir(os.path.join(dbroot, fcc))\r
1391             mdir.add(msg)\r
1392             mdir.close()\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
1396         for key in ret:\r
1397             print 'Error sending mail to %s: %s'%(key, ret[key])\r
1398         s.quit()\r
1399 \r
1400 class NMBufferElement(object):\r
1401     """\r
1402     This object represents a structure (e.g. folder, thread or message)\r
1403     corresponding to a range of lines in NMBuffer.\r
1404     """\r
1405     # start and end lines, 0-based\r
1406     start =3D None\r
1407     end   =3D None\r
1408     # a string identifying the element\r
1409     id    =3D None\r
1410 \r
1411     def __init__(self, start, end, id):\r
1412         self.start =3D start\r
1413         self.end   =3D end\r
1414         self.id    =3D id\r
1415 \r
1416 class Folder(NMBufferElement):\r
1417     """\r
1418     A saved search / 'folder'.\r
1419     """\r
1420     # folder name\r
1421     name =3D None\r
1422 \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
1426 \r
1427 class Message(NMBufferElement):\r
1428 \r
1429     attachments =3D None\r
1430 \r
1431     def __init__(self, start, end, id):\r
1432         super(Message, self).__init__(start, end, id)\r
1433         self.attachments =3D []\r
1434 \r
1435 class Attachment(NMBufferElement):\r
1436     data =3D None\r
1437 \r
1438     def __init__(self, start, end, data):\r
1439         super(Attachment, self).__init__(start, end, '')\r
1440         self. data =3D data\r
1441 \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
1445 nm_buffers =3D {}\r
1446 \r
1447 #### utility functions ####\r
1448 \r
1449 def get_current_buffer():\r
1450     """\r
1451     Get the NMBuffer object associated with current buffer or None.\r
1452     """\r
1453     try:\r
1454         return nm_buffers[vim.eval('bufnr("%")')]\r
1455     except KeyError:\r
1456         return None\r
1457 \r
1458 def delete_current_buffer():\r
1459     """\r
1460     Delete the NMBuffer associated with current buffer.\r
1461     """\r
1462     del nm_buffers[vim.eval('bufnr("%")')]\r
1463 \r
1464 def get_relative_date(timestamp):\r
1465     """\r
1466     Format a nice representation of 'time' relative to the current time.\r
1467 \r
1468      Examples include:\r
1469 \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
1472         Yest. 12:30\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
1476 \r
1477      Shamelessly lifted from notmuch-time\r
1478      TODO: this should probably be in python-notmuch\r
1479      """\r
1480     try:\r
1481         now  =3D datetime.datetime.now()\r
1482         then =3D datetime.datetime.fromtimestamp(timestamp)\r
1483     except ValueError:\r
1484         return "when?"\r
1485 \r
1486 \r
1487     if then > now:\r
1488         return "the future"\r
1489 \r
1490     delta =3D now - then\r
1491 \r
1492     if delta.days > 180:\r
1493         return then.strftime("%F") # 2008-06-30\r
1494 \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
1498 \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
1505 \r
1506     return then.strftime("%B %d")        # October 12\r
1507 \r
1508 def map_query(db, query, run):\r
1509     """\r
1510     Execute runnable run on every message found for the specified query.\r
1511     """\r
1512     def walk_query(message, run, level):\r
1513         run(message, level)\r
1514         level +=3D 1\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
1519 \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
1524 \r
1525 def decode_header(header):\r
1526     """\r
1527     Decode a RFC 2822 header into a utf-8 string.\r
1528     """\r
1529     ret =3D []\r
1530     for part in email.header.decode_header(header):\r
1531         if part[1]:\r
1532             ret.append(part[0].decode(part[1]).encode('utf-8'))\r
1533         else:\r
1534             ret.append(part[0])\r
1535     return ''.join(ret)\r
1536 \r
1537 def get_author(address):\r
1538     """\r
1539     Extract the author name from an address if possible.\r
1540     """\r
1541     #XXX should be in the bindings\r
1542     if address.endswith('>'):\r
1543         try:\r
1544             ret =3D address[:address.rindex('<') - 1].strip()\r
1545             if ret:\r
1546                 return ret\r
1547         except ValueError:\r
1548             pass\r
1549     return address\r
1550 \r
1551 #### functions for exporting stuff to viml ####\r
1552 \r
1553 def vim_get_tags():\r
1554     """\r
1555     Export a string listing all tags in the database, one per line into\r
1556     a vim variable named 'taglist'.\r
1557     """\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
1561 \r
1562 def vim_get_object(line, offset):\r
1563     """\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
1569     """\r
1570     # vim lines are 1-based\r
1571     line -=3D 1\r
1572     assert line >=3D 0\r
1573 \r
1574     obj =3D get_current_buffer().get_object(line, offset)\r
1575     if obj:\r
1576         vim.command('let obj =3D { "start" : %d, "end" : %d, "id" : "%s" }'=\r
1577 %(\r
1578                     obj.start + 1, obj.end + 1, obj.id))\r
1579 \r
1580 def vim_get_id():\r
1581     """\r
1582     Export the id string of current buffer into a vim string named 'buf_id'.\r
1583     """\r
1584     id =3D get_current_buffer().id\r
1585     vim.command('let buf_id =3D "%s"'%(id if id else ''))\r
1586 \r
1587 def vim_view_attachment(line):\r
1588     """\r
1589     View an attachment corresponding to the given vim line\r
1590     in an external viewer.\r
1591     """\r
1592     line -=3D 1       # vim lines are 1-based\r
1593     get_current_buffer().view_attachment(line)\r
1594 \r
1595 --===============0474093823==\r
1596 Content-Type: text/plain; charset="utf-8"\r
1597 MIME-Version: 1.0\r
1598 Content-Transfer-Encoding: quoted-printable\r
1599 Content-Disposition: attachment; filename="notmuch-folders.vim"\r
1600 \r
1601 " notmuch folders mode syntax file\r
1602 \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
1612 \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
1617 \r
1618 highlight CursorLine term=3Dreverse cterm=3Dreverse gui=3Dreverse\r
1619 \r
1620 \r
1621 --===============0474093823==\r
1622 Content-Type: text/plain; charset="utf-8"\r
1623 MIME-Version: 1.0\r
1624 Content-Transfer-Encoding: quoted-printable\r
1625 Content-Disposition: attachment; filename="notmuch-search.vim"\r
1626 \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
1639 =3DnmSearchTags\r
1640 syntax match nmSearchTags       /.\+$/                  contained\r
1641 \r
1642 syntax match nmUnread           /^.*(.*\<unread\>.*)$/\r
1643 \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
1650 \r
1651 highlight link nmUnread        Underlined\r
1652 \r
1653 --===============0474093823==\r
1654 Content-Type: text/plain; charset="utf-8"\r
1655 MIME-Version: 1.0\r
1656 Content-Transfer-Encoding: quoted-printable\r
1657 Content-Disposition: attachment; filename="notmuch-show.vim"\r
1658 \r
1659 " notmuch show mode syntax file\r
1660 \r
1661 setlocal conceallevel=3D2\r
1662 setlocal concealcursor=3Dvinc\r
1663 \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
1666 wMsgBody keepend\r
1667 \r
1668 "TODO what about those\r
1669 syntax cluster nmShowMsgDesc contains=3DnmShowMsgDescWho,nmShowMsgDescDate,=\r
1670 nmShowMsgDescTags\r
1671 syntax match   nmShowMsgDescWho /[^)]\+)/ contained\r
1672 syntax match   nmShowMsgDescDate / ([^)]\+[0-9]) / contained\r
1673 syntax match   nmShowMsgDescTags /([^)]\+)$/ contained\r
1674 \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
1678 \r
1679 highlight nmShowMsgDescWho term=3Dreverse cterm=3Dreverse gui=3Dreverse\r
1680 highlight link nmShowMsgDescDate Type\r
1681 highlight link nmShowMsgDescTags String\r
1682 \r
1683 "TODO what about this?\r
1684 highlight Folded term=3Dreverse ctermfg=3DLightGrey ctermbg=3DBlack guifg=\r
1685 =3DLightGray guibg=3DBlack\r
1686 \r
1687 --===============0474093823==--\r