From e23e363e0a999bc3528b6baab84e203b46e1de7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 3 Apr 2021 12:12:26 +0200 Subject: [PATCH] import vim plugin --- .gitignore | 1 + vim/README.md | 125 +++++++++++++++ vim/autoload/himalaya/mbox.vim | 57 +++++++ vim/autoload/himalaya/mbx.vim | 107 ++++++++++++ vim/autoload/himalaya/msg.vim | 241 ++++++++++++++++++++++++++++ vim/autoload/himalaya/shared.vim | 28 ++++ vim/autoload/himalaya/ui.vim | 238 +++++++++++++++++++++++++++ vim/autoload/himalaya/utils.vim | 87 ++++++++++ vim/doc/himalaya.txt | 149 +++++++++++++++++ vim/ftplugin/himalaya-msg-list.vim | 21 +++ vim/ftplugin/himalaya-msg-read.vim | 18 +++ vim/ftplugin/himalaya-msg-write.vim | 13 ++ vim/plugin/himalaya.vim | 11 ++ vim/syntax/himalaya-msg-list.vim | 24 +++ 14 files changed, 1120 insertions(+) create mode 100644 vim/README.md create mode 100644 vim/autoload/himalaya/mbox.vim create mode 100644 vim/autoload/himalaya/mbx.vim create mode 100644 vim/autoload/himalaya/msg.vim create mode 100644 vim/autoload/himalaya/shared.vim create mode 100644 vim/autoload/himalaya/ui.vim create mode 100644 vim/autoload/himalaya/utils.vim create mode 100644 vim/doc/himalaya.txt create mode 100644 vim/ftplugin/himalaya-msg-list.vim create mode 100644 vim/ftplugin/himalaya-msg-read.vim create mode 100644 vim/ftplugin/himalaya-msg-write.vim create mode 100644 vim/plugin/himalaya.vim create mode 100644 vim/syntax/himalaya-msg-list.vim diff --git a/.gitignore b/.gitignore index ea8c4bf..28b27a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/vim/doc/tags diff --git a/vim/README.md b/vim/README.md new file mode 100644 index 0000000..aa6fa16 --- /dev/null +++ b/vim/README.md @@ -0,0 +1,125 @@ +# šŸ“« Himalaya.vim + +Vim plugin for [Himalaya](https://github.com/soywod/himalaya) CLI email client. + +![image](https://user-images.githubusercontent.com/10437171/104848096-aee51000-58e3-11eb-8d99-bcfab5ca28ba.png) + +## Table of contents + +* [Motivation](#motivation) +* [Installation](#installation) +* [Usage](#usage) + * [List messages view](#list-messages-view) + * [Read message view](#read-message-view) + * [Write message view](#write-message-view) +* [License](https://github.com/soywod/himalaya.vim/blob/master/LICENSE) +* [Credits](#credits) + +## Motivation + +Bringing emails to the terminal is a pain. The mainstream TUI, (neo)mutt, takes +time to configure. The default mapping is not intuitive when coming from the +Vim environment. It is even scary to use at the beginning, since you are +dealing with sensitive data! + +The aim of Himalaya is to extract the email logic into a simple (yet solid) CLI +API that can be used either directly from the terminal or UIs. It gives users +more flexibility. + +This Vim plugin is a TUI implementation for Himalaya CLI. + +## Installation + +First you need to install and configure the [himalaya +CLI](https://github.com/soywod/himalaya#installation). Then you can install +this plugin with your favorite plugin manager. For eg with +[vim-plug](https://github.com/junegunn/vim-plug) add to your `.vimrc`: + +```viml +Plug "soywod/himalaya.vim" +``` + +Then: + +```viml +:PlugInstall +``` + +## Usage + +### List messages view + +```vim +:Himalaya +``` + +![gif](https://user-images.githubusercontent.com/10437171/110707014-f9ef1580-81f8-11eb-93ad-233010733ca3.gif) + +| Function | Default binding | +| --- | --- | +| Change the current mbox | `gm` | +| Show previous page | `gp` | +| Show next page | `gn` | +| Read focused msg | `` | +| Write a new msg | `gw` | +| Reply to the focused msg | `gr` | +| Reply all to the focused msg | `gR` | +| Forward the focused message | `gf` | +| Download all focused msg attachments | `ga` | + +They can be customized: + +```vim +nmap gm (himalaya-mbox-input) +nmap gp (himalaya-mbox-prev-page) +nmap gn (himalaya-mbox-next-page) +nmap (himalaya-msg-read) +nmap gw (himalaya-msg-write) +nmap gr (himalaya-msg-reply) +nmap gR (himalaya-msg-reply-all) +nmap gf (himalaya-msg-forward) +nmap ga (himalaya-msg-attachments) +``` + +### Read message view + +![gif](https://user-images.githubusercontent.com/10437171/110708073-7b937300-81fa-11eb-9f4c-5472cea22e21.gif) + +| Function | Default binding | +| --- | --- | +| Write a new msg | `gw` | +| Reply to the msg | `gr` | +| Reply all to the msg | `gR` | +| Forward the message | `gf` | +| Download all msg attachments | `ga` | + +They can be customized: + +```vim +nmap gw (himalaya-msg-write) +nmap gr (himalaya-msg-reply) +nmap gR (himalaya-msg-reply-all) +nmap gf (himalaya-msg-forward) +nmap ga (himalaya-msg-attachments) +``` + +### Write message view + +![gif](https://user-images.githubusercontent.com/10437171/110708795-84387900-81fb-11eb-8f8a-f7e7862e816d.gif) + +When you exit this special buffer, you will be prompted 4 choices: + +- `Send`: sends the message +- `Draft`: saves the message into the `Drafts` mailbox +- `Quit`: quits the buffer without saving +- `Cancel`: goes back to the message edition + +## Credits + +- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501) +- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor +- [isync](https://isync.sourceforge.io/), an email synchronizer for offline usage +- [NeoMutt](https://neomutt.org/), an email terminal user interface +- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other email terminal user interface +- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool over NeoMutt and isync +- [rust-imap](https://github.com/jonhoo/rust-imap), a rust IMAP lib diff --git a/vim/autoload/himalaya/mbox.vim b/vim/autoload/himalaya/mbox.vim new file mode 100644 index 0000000..9e003df --- /dev/null +++ b/vim/autoload/himalaya/mbox.vim @@ -0,0 +1,57 @@ +let s:print_info = function("himalaya#utils#print_msg") +let s:print_err = function("himalaya#utils#print_err") +let s:cli = function("himalaya#shared#cli") + +" Pagination + +let s:curr_page = 0 +function! himalaya#mbox#curr_page() + return s:curr_page +endfunction + +function! himalaya#mbox#prev_page() + let s:curr_page = max([0, s:curr_page - 1]) + call himalaya#msg#list() +endfunction + +function! himalaya#mbox#next_page() + let s:curr_page = s:curr_page + 1 + call himalaya#msg#list() +endfunction + +" Mailbox + +let s:curr_mbox = "INBOX" +function! himalaya#mbox#curr_mbox() + return s:curr_mbox +endfunction + +function! himalaya#mbox#input() + try + call s:print_info("Fetching mailboxesā€¦") + + let mboxes = map(s:cli("mailboxes", []), "v:val.name") + + " if &rtp =~ "fzf.vim" + " call fzf#run({ + " \"source": mboxes, + " \"sink": function("himalaya#mbox#post_input"), + " \"down": "25%", + " \}) + " else + let choice = map(copy(mboxes), "printf('%s (%d)', v:val, v:key)") + redraw | echo + let choice = input(join(choice, ", ") . ": ") + redraw | echo + call himalaya#mbox#post_input(mboxes[choice]) + " endif + catch + call s:print_err(v:exception) + endtry +endfunction + +function! himalaya#mbox#post_input(mbox) + let s:curr_mbox = a:mbox + let s:curr_page = 0 + call himalaya#msg#list() +endfunction diff --git a/vim/autoload/himalaya/mbx.vim b/vim/autoload/himalaya/mbx.vim new file mode 100644 index 0000000..72a3ab4 --- /dev/null +++ b/vim/autoload/himalaya/mbx.vim @@ -0,0 +1,107 @@ +let s:trim = function("himalaya#utils#trim") +let s:print_err = function("himalaya#utils#print_err") +let s:print_info = function("himalaya#utils#print_msg") + +let s:buff_name = "Himalaya" + +" Exec utils + +function! s:exec(cmd, args) + let cmd = call("printf", [a:cmd] + a:args) + let res = system(cmd) + + try + return eval(res) + catch + throw res + endtry +endfunction + +" Render utils + +function! s:render(type, lines) + let s:max_widths = s:get_max_widths(a:lines, s:config[a:type].columns) + let header = [s:render_line(s:config.labels, s:max_widths, a:type)] + let line = map(copy(a:lines), "s:render_line(v:val, s:max_widths, a:type)") + + return header + line +endfunction + +function! s:render_line(line, max_widths, type) + return "|" . join(map( + \copy(s:config[a:type].columns), + \"s:render_cell(a:line[v:val], a:max_widths[v:key])", + \), "") +endfunction + +function! s:render_cell(cell, max_width) + let cell_width = strdisplaywidth(a:cell[:a:max_width]) + return a:cell[:a:max_width] . repeat(" ", a:max_width - cell_width) . " |" +endfunction + +function! s:get_max_widths(msgs, columns) + let max_widths = map(copy(a:columns), "strlen(s:config.labels[v:val])") + + for msg in a:msgs + let widths = map(copy(a:columns), "strlen(msg[v:val])") + call map(max_widths, "max([widths[v:key], v:val])") + endfor + + return max_widths +endfunction + +" List + +let s:config = { + \"list": { + \"columns": ["delim", "name", "attributes"], + \}, + \"labels": { + \"delim": "DELIM", + \"name": "NAME", + \"attributes": "ATTRIBUTES", + \}, +\} + +function! himalaya#mbx#format_for_list(mbx) + let mbx = copy(a:mbx) + let mbx.attributes = join(mbx.attributes, ", ") + return mbx +endfunction + +function! s:get_focused_mbx() + try + return s:trim(split(getline("."), "|")[1]) + catch + throw "mailbox not found" + endtry +endfunction + +function! himalaya#mbx#list() + try + call s:print_info("Fetching mailboxesā€¦") + + let prev_pos = getpos(".") + let mbxs = s:exec("himalaya --output json mailboxes", []) + let mbxs = map(copy(mbxs), 'himalaya#mbx#format_for_list(v:val)') + + silent! bwipeout "Mailboxes" + silent! edit Mailboxes + + call append(0, s:render("list", mbxs)) + execute "$d" + + call setpos(".", prev_pos) + setlocal filetype=himalaya-mbx-list + let &modified = 0 + + call s:print_info("Done!") + catch + call s:print_err(v:exception) + endtry +endfunction + +function! himalaya#mbx#select() + let mbx = s:get_focused_mbx() + call himalaya#msg#list(mbx) +endfunction diff --git a/vim/autoload/himalaya/msg.vim b/vim/autoload/himalaya/msg.vim new file mode 100644 index 0000000..7510db3 --- /dev/null +++ b/vim/autoload/himalaya/msg.vim @@ -0,0 +1,241 @@ +let s:print_info = function("himalaya#utils#print_msg") +let s:print_err = function("himalaya#utils#print_err") +let s:trim = function("himalaya#utils#trim") +let s:cli = function("himalaya#shared#cli") + +let s:msg_id = 0 +let s:draft = "" + +" Message + +function! s:format_msg_for_list(msg) + let msg = copy(a:msg) + + let flag_unseen = index(msg.flags, "Seen") == -1 ? "šŸŸ“" : " " + let flag_replied = index(msg.flags, "Answered") == -1 ? " " : "ā†©" + let flag_flagged = index(msg.flags, "Flagged") == -1 ? " " : "!" + let msg.flags = printf("%s%s%s", flag_unseen, flag_replied, flag_flagged) + + return msg +endfunction + +function! himalaya#msg#list() + try + let mbox = himalaya#mbox#curr_mbox() + let page = himalaya#mbox#curr_page() + + call s:print_info(printf("Fetching %s messagesā€¦", tolower(mbox))) + let msgs = s:cli("--mailbox %s list --page %d", [shellescape(mbox), page]) + let msgs = map(copy(msgs), "s:format_msg_for_list(v:val)") + call s:print_info("Done!") + + let buftype = stridx(bufname("%"), "Himalaya messages") == 0 ? "file" : "edit" + execute printf("silent! %s Himalaya messages [%s] [page %d]", buftype, tolower(mbox), page + 1) + setlocal modifiable + execute "%d" + call append(0, s:render("list", msgs)) + execute "$d" + setlocal filetype=himalaya-msg-list + let &modified = 0 + execute 0 + catch + call s:print_err(v:exception) + endtry +endfunction + +function! himalaya#msg#read() + try + let s:msg_id = s:get_focused_msg_id() + let mbox = himalaya#mbox#curr_mbox() + + call s:print_info(printf("Fetching message %dā€¦", s:msg_id)) + let msg = s:cli("read %d --mailbox %s", [s:msg_id, shellescape(mbox)]) + call s:print_info("Done!") + + let attachment = msg.hasAttachment ? " [ļƒ†]" : "" + execute printf("silent! edit Himalaya read message [%d]%s", s:msg_id, attachment) + setlocal modifiable + execute "%d" + call append(0, split(substitute(msg.content, "\r", "", "g"), "\n")) + execute "$d" + setlocal filetype=himalaya-msg-read + let &modified = 0 + execute 0 + catch + call s:print_err(v:exception) + endtry +endfunction + +function! himalaya#msg#write() + try + call s:print_info("Fetching new templateā€¦") + let msg = s:cli("template new", []) + call s:print_info("Done!") + + silent! edit Himalaya write + call append(0, split(substitute(msg.template, "\r", "", "g"), "\n")) + execute "$d" + setlocal filetype=himalaya-msg-write + let &modified = 0 + execute 0 + catch + call s:print_err(v:exception) + endtry +endfunction + +function! himalaya#msg#reply() + try + let mbox = himalaya#mbox#curr_mbox() + let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id + + call s:print_info("Fetching reply templateā€¦") + let msg = s:cli("template reply %d --mailbox %s", [msg_id, shellescape(mbox)]) + call s:print_info("Done!") + + execute printf("silent! edit Himalaya reply [%d]", msg_id) + call append(0, split(substitute(msg.template, "\r", "", "g"), "\n")) + execute "$d" + setlocal filetype=himalaya-msg-write + let &modified = 0 + execute 0 + catch + call s:print_err(v:exception) + endtry +endfunction + +function! himalaya#msg#reply_all() + try + let mbox = himalaya#mbox#curr_mbox() + let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id + + call s:print_info("Fetching reply all templateā€¦") + let msg = s:cli("template reply %d --mailbox %s --all", [msg_id, shellescape(mbox)]) + call s:print_info("Done!") + + execute printf("silent! edit Himalaya reply all [%d]", msg_id) + call append(0, split(substitute(msg.template, "\r", "", "g"), "\n")) + execute "$d" + setlocal filetype=himalaya-msg-write + let &modified = 0 + execute 0 + catch + call s:print_err(v:exception) + endtry +endfunction + +function! himalaya#msg#forward() + try + let mbox = himalaya#mbox#curr_mbox() + let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id + + call s:print_info("Fetching forward templateā€¦") + let msg = s:cli("template forward %d --mailbox %s", [msg_id, shellescape(mbox)]) + call s:print_info("Done!") + + execute printf("silent! edit Himalaya forward [%d]", msg_id) + call append(0, split(substitute(msg.template, "\r", "", "g"), "\n")) + execute "$d" + setlocal filetype=himalaya-msg-write + let &modified = 0 + execute 0 + catch + call s:print_err(v:exception) + endtry +endfunction + +function! himalaya#msg#draft_save() + let s:draft = join(getline(1, "$"), "\r\n") + call s:print_info("Draft saved!") + let &modified = 0 +endfunction + +function! himalaya#msg#draft_handle() + while 1 + let choice = input("(s)end, (d)raft, (q)uit or (c)ancel? ") + let choice = tolower(choice)[0] + redraw | echo + + if choice == "s" + call s:print_info("Sending messageā€¦") + call s:cli("send -- %s", [shellescape(s:draft)]) + call s:print_info("Done!") + return + elseif choice == "d" + call s:print_info("Saving draftā€¦") + call s:cli("save --mailbox Drafts -- %s", [shellescape(s:draft)]) + call s:print_info("Done!") + return + elseif choice == "q" + return + elseif choice == "c" + throw "Action canceled" + endif + endwhile +endfunction + +function! himalaya#msg#attachments() + try + let mbox = himalaya#mbox#curr_mbox() + let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id + + call s:print_info("Downloading attachmentsā€¦") + let msg = s:cli("attachments %d --mailbox %s", [msg_id, shellescape(mbox)]) + call s:print_info("Done!") + catch + call s:print_err(v:exception) + endtry +endfunction + +" Render utils + +let s:config = { + \"list": { + \"columns": ["uid", "flags", "subject", "sender", "date"], + \}, + \"labels": { + \"uid": "UID", + \"flags": "FLAGS", + \"subject": "SUBJECT", + \"sender": "SENDER", + \"date": "DATE", + \}, +\} + +function! s:render(type, lines) + let s:max_widths = s:get_max_widths(a:lines, s:config[a:type].columns) + let header = [s:render_line(s:config.labels, s:max_widths, a:type)] + let line = map(copy(a:lines), "s:render_line(v:val, s:max_widths, a:type)") + + return header + line +endfunction + +function! s:render_line(line, max_widths, type) + return "|" . join(map( + \copy(s:config[a:type].columns), + \"s:render_cell(a:line[v:val], a:max_widths[v:key])", + \), "") +endfunction + +function! s:render_cell(cell, max_width) + let cell_width = strdisplaywidth(a:cell[:a:max_width]) + return a:cell[:a:max_width] . repeat(" ", a:max_width - cell_width) . " |" +endfunction + +function! s:get_max_widths(msgs, columns) + let max_widths = map(copy(a:columns), "strlen(s:config.labels[v:val])") + + for msg in a:msgs + let widths = map(copy(a:columns), "strlen(msg[v:val])") + call map(max_widths, "max([widths[v:key], v:val])") + endfor + + return max_widths +endfunction + +function! s:get_focused_msg_id() + try + return s:trim(split(getline("."), "|")[0]) + catch + throw "message not found" + endtry +endfunction diff --git a/vim/autoload/himalaya/shared.vim b/vim/autoload/himalaya/shared.vim new file mode 100644 index 0000000..78b13d2 --- /dev/null +++ b/vim/autoload/himalaya/shared.vim @@ -0,0 +1,28 @@ +function! himalaya#shared#define_bindings(bindings) + for [mode, key, name] in a:bindings + let plug = substitute(name, "[#_]", "-", "g") + let plug = printf("(himalaya-%s)", plug) + execute printf("%snoremap %s :call himalaya#%s()", mode, plug, name) + + if !hasmapto(plug, mode) + execute printf("%smap %s %s", mode, key, plug) + endif + endfor +endfunction + +function! himalaya#shared#cli(cmd, args) + let cmd = call("printf", ["himalaya --output json " . a:cmd] + a:args) + let res = system(cmd) + + if !empty(res) + try + return eval(res) + catch + throw res + endtry + endif +endfunction + +function! himalaya#shared#thread_fold(lnum) + return getline(a:lnum)[0] == ">" +endfunction diff --git a/vim/autoload/himalaya/ui.vim b/vim/autoload/himalaya/ui.vim new file mode 100644 index 0000000..a6e1a87 --- /dev/null +++ b/vim/autoload/himalaya/ui.vim @@ -0,0 +1,238 @@ +let s:compose = function('himalaya#utils#compose') +let s:trim = function('himalaya#utils#trim') +let s:print_msg = function('himalaya#utils#print_msg') +let s:print_err = function('himalaya#utils#print_err') + +let s:max_widths = [] +let s:buff_name = 'Himalaya' +let s:msgs = [] + +let s:config = { + \'list': { + \'columns': ['uid', 'subject', 'sender', 'date'], + \}, + \'labels': { + \'uid': 'ID', + \'subject': 'SUBJECT', + \'sender': 'SENDER', + \'date': 'DATE', + \}, +\} + +function! himalaya#ui#list() + try + let prev_pos = getpos('.') + let s:msgs = himalaya#msg#list() + let lines = map(copy(s:msgs), 'himalaya#msg#format_for_list(v:val)') + + redir => buf_list | silent! ls | redir END + execute 'silent! edit ' . s:buff_name + + if match(buf_list, '"Himalaya') > -1 + execute '0,$d' + endif + + call append(0, s:render('list', lines)) + execute '$d' + call setpos('.', prev_pos) + setlocal filetype=himalaya-list + let &modified = 0 + echo + catch + call s:print_err(v:exception) + endtry +endfunction + +" Cell management + +function! himalaya#ui#select_next_cell() + normal! f|l + + if col('.') == col('$') - 1 + if line('.') == line('$') + normal! T| + else + normal! j0l + endif + endif +endfunction + +function! himalaya#ui#select_prev_cell() + if col('.') == 2 && line('.') > 2 + normal! k$T| + else + normal! 2T| + endif +endfunction + +function! himalaya#ui#delete_in_cell() + execute printf('normal! %sdt|', col('.') == 1 ? '' : 'T|') +endfunction + +function! himalaya#ui#change_in_cell() + call himalaya#ui#delete_in_cell() + startinsert +endfunction + +function! himalaya#ui#visual_in_cell() + execute printf('normal! %svt|', col('.') == 1 ? '' : 'T|') +endfunction + +" Parse utils + +function! himalaya#ui#parse_buffer() + " try + " let lines = filter(getline(2, "$"), "!empty(s:trim(v:val))") + " let prev_msgs = copy(s:msgs) + " let next_msgs = map(lines, "s:parse_buffer_line(v:key, v:val)") + " let msgs_to_add = filter(copy(next_msgs), "empty(v:val.id)") + " let msgs_to_edit = [] + " let msgs_to_do = [] + " let msgs = [] + + " for prev_msg in prev_msgs + " let next_msg = filter(copy(next_msgs), "v:val.id == prev_msg.id") + + " if empty(next_msg) + " let msgs_to_do += [prev_msg.id] + " elseif prev_msg.desc != next_msg[0].desc || prev_msg.project != next_msg[0].project || prev_msg.due.approx != next_msg[0].due + " let msgs_to_edit += [next_msg[0]] + " endif + " endfor + + " for msg in msgs_to_add | let msgs += [himalaya#msg#add(msg)] | endfor + " for msg in msgs_to_edit | let msgs += [himalaya#msg#edit(msg)] | endfor + " for id in msgs_to_do | let msgs += [himalaya#msg#do(id)] | endfor + + " call himalaya#ui#list() + " let &modified = 0 + " for msg in msgs | call s:print_msg(msg) | endfor + " catch + " call s:print_err(v:exception) + " endtry +endfunction + +function! s:parse_buffer_line(index, line) + if match(a:line, '^|[0-9a-f\-]\{-} *|.* *|.\{-} *|.\{-} *|.\{-} *|$') != -1 + let cells = split(a:line, "|") + let id = s:trim(cells[0]) + let desc = s:trim(join(cells[1:-4], "")) + let project = s:trim(cells[-3]) + let due = s:trim(cells[-1]) + + return { + \"id": id, + \"desc": desc, + \"project": project, + \"due": due, + \} + else + let [desc, project, due] = s:parse_args(s:trim(a:line)) + + return { + \"id": "", + \"desc": desc, + \"project": project, + \"due": due, + \} + endif +endfunction + +function! s:uniq_by_id(a, b) + if a:a.id > a:b.id | return 1 + elseif a:a.id < a:b.id | return -1 + else | return 0 | endif +endfunction + +function! s:parse_args(args) + let args = split(a:args, ' ') + + let idx = 0 + let desc = [] + let project = "" + let due = "" + + while idx < len(args) + let arg = args[idx] + + if arg == "-p" || arg == "--project" + let project = get(args, idx + 1, "") + let idx = idx + 1 + elseif arg == "-d" || arg == "--due" + let due = get(args, idx + 1, "") + let idx = idx + 1 + else + call add(desc, arg) + endif + + let idx = idx + 1 + endwhile + + return [join(desc, ' '), project, due] +endfunction + +" ------------------------------------------------------------------ # Renders # + +function! s:render(type, lines) + let s:max_widths = s:get_max_widths(a:lines, s:config[a:type].columns) + let header = [s:render_line(s:config.labels, s:max_widths, a:type)] + let line = map(copy(a:lines), 's:render_line(v:val, s:max_widths, a:type)') + + return header + line +endfunction + +function! s:render_line(line, max_widths, type) + return '|' . join(map( + \copy(s:config[a:type].columns), + \'s:render_cell(a:line[v:val], a:max_widths[v:key])', + \), '') +endfunction + +function! s:render_cell(cell, max_width) + let cell_width = strdisplaywidth(a:cell[:a:max_width]) + return a:cell[:a:max_width] . repeat(' ', a:max_width - cell_width) . ' |' +endfunction + +" -------------------------------------------------------------------- # Utils # + +function! s:get_max_widths(msgs, columns) + let max_widths = map(copy(a:columns), 'strlen(s:config.labels[v:val])') + + for msg in a:msgs + let widths = map(copy(a:columns), 'strlen(msg[v:val])') + call map(max_widths, 'max([widths[v:key], v:val])') + endfor + + return max_widths +endfunction + +function! s:get_focused_msg_id() + try + return s:trim(split(getline("."), "|")[0]) + catch + throw "msg not found" + endtry +endfunction + +function! s:refresh_buff_name() + let buff_name = 'Himalaya' + + if !g:himalaya_hide_done + let buff_name .= '*' + endif + + if len(g:himalaya_context) > 0 + let tags = map(copy(g:himalaya_context), 'printf(" +%s", v:val)') + let buff_name .= join(tags, '') + endif + + if buff_name != s:buff_name + execute 'silent! enew' + execute 'silent! bwipeout ' . s:buff_name + let s:buff_name = buff_name + endif +endfunction + +function! s:exists_in(list, item) + return index(a:list, a:item) > -1 +endfunction diff --git a/vim/autoload/himalaya/utils.vim b/vim/autoload/himalaya/utils.vim new file mode 100644 index 0000000..b7b7f92 --- /dev/null +++ b/vim/autoload/himalaya/utils.vim @@ -0,0 +1,87 @@ +" ------------------------------------------------------------------ # Compose # + +function! himalaya#utils#compose(...) + let funcs = map(reverse(copy(a:000)), 'function(v:val)') + return function('s:compose', [funcs]) +endfunction + +function! s:compose(funcs, arg) + let data = a:arg + + for Func in a:funcs + let data = Func(data) + endfor + + return data +endfunction + +" --------------------------------------------------------------------- # Trim # + +function! himalaya#utils#trim(str) + return himalaya#utils#compose('s:trim_left', 's:trim_right')(a:str) +endfunction + +function! s:trim_left(str) + return substitute(a:str, '^\s*', '', 'g') +endfunction + +function! s:trim_right(str) + return substitute(a:str, '\s*$', '', 'g') +endfunction + +" ------------------------------------------------------------------- # Assign # + +function! himalaya#utils#assign(...) + let overrides = copy(a:000) + let base = remove(overrides, 0) + + for override in overrides + for [key, val] in items(override) + let base[key] = val + unlet key val + endfor + endfor + + return base +endfunction + +" ---------------------------------------------------------------------- # Sum # + +function! himalaya#utils#sum(array) + let total = 0 + + for item in a:array + let total += item + endfor + + return total +endfunction + +" ----------------------------------------------------------- # Match one item # + +function! himalaya#utils#match_one(list_src, list_dest) + if empty(a:list_dest) + return 1 + endif + + for item in a:list_src + if index(a:list_dest, item) > -1 | return 1 | endif + endfor + + return 0 +endfunction + + +" --------------------------------------------------------------------- # Logs # + +function! himalaya#utils#print_msg(msg) + echohl None + echom a:msg +endfunction + +function! himalaya#utils#print_err(err) + redraw + echohl ErrorMsg + echom a:err + echohl None +endfunction diff --git a/vim/doc/himalaya.txt b/vim/doc/himalaya.txt new file mode 100644 index 0000000..6c28cfb --- /dev/null +++ b/vim/doc/himalaya.txt @@ -0,0 +1,149 @@ +*himalaya.txt* - ā± Minimalist task & time manager. + + _/ _/ _/ _/ _/_/_/_/ _/_/ _/_/_/ + _/ _/ _/_/ _/ _/ _/ _/ _/ + _/ _/ _/ _/ _/ _/_/_/ _/ _/ _/ _/_/ + _/ _/ _/ _/_/ _/ _/ _/ _/ _/ + _/_/ _/ _/ _/ _/_/ _/_/_/ + +============================================================================== +TABLE OF CONTENTS *himalaya-contents* + + Requirements .......................................... |himalaya-requirements| + Usage ........................................................ |himalaya-usage| + Mappings ................................................. |himalaya-mappings| + License .................................................... |himalaya-license| + Contributing .......................................... |himalaya-contributing| + Changelog ................................................ |himalaya-changelog| + Credits .................................................... |himalaya-credits| + +============================================================================== +REQUIREMENTS *himalaya-requirements* + + - Vim or Neovim + - Himalaya CLI {1} + https://github.com/soywod/himalaya#installation {1} + +============================================================================== +USAGE *himalaya-usage* + +It is recommanded to first read the Himalaya CLI documentation {1} to understand +the concept. + +To list tasks: +> + :Himalaya +< +Then you can manage tasks using Vim mapping. The table will automatically +readjust on buffer save (`:w`). See https://github.com/soywod/himalaya.vim. + + https://github.com/soywod/himalaya#readme {1} + +============================================================================== +MAPPINGS *himalaya-mappings* + +Here the default mappings: + +List done tasks gd +List deleted tasks gD +Toggle task +Show task infos {K} +Set context {gc} +Show worktime {gw} +Jump to the next cell +Jump to the prev cell +Delete in cell {dic} +Change in cell {cic} +Visual in cell {vic} + +You can customize them: +> + nmap gd (himalaya-list-done) + nmap gD (himalaya-list-deleted) + nmap (himalaya-toggle) + nmap K (himalaya-info) + nmap gc (himalaya-context) + nmap gw (himalaya-worktime) + nmap (himalaya-next-cell) + nmap (himalaya-prev-cell) + nmap dic (himalaya-delete-in-cell) + nmap cic (himalaya-change-in-cell) + nmap vic (himalaya-visual-in-cell) +< +============================================================================== +LICENSE *himalaya-license* + +Copyright Ā© 2019-2020 ClĆ©ment DOUIN + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +============================================================================== +CONTRIBUTING *himalaya-contributing* + +Report any bugs here: https://github.com/soywod/himalaya.vim/issues +Feel free to submit pull requests: https://github.com/soywod/himalaya.vim/pulls + +1) Git commit messages follow the `Angular Convention` {1}, but contain only a +subject. +> + - Use imperative, present tense: ā€œchangeā€ not ā€œchangedā€ nor ā€œchangesā€ + - Don't capitalize first letter + - No dot (.) at the end +< +2) Vim code should be as clean as possible, variables and functions use the +snake case convention. A line should never contain more than `80` characters. + +3) Tests should be added for each new functionality. Be sure to run tests +before proposing a pull request. + + https://gist.github.com/stephenparish/9941e89d80e2bc58a153 {1} + +============================================================================== +CHANGELOG *himalaya-changelog* + +See https://github.com/soywod/himalaya.vim/blob/master/CHANGELOG.md. + +============================================================================== +CREDITS *himalaya-credits* + + - Taskwarrior, a task manager {1} + - Timewarrior, a time tracker {2} + - vim-taskwarrior, a Taskwarrior wrapper for vim {3} + - vimwiki, for the idea of managing tasks inside a buffer {4} + - Kronos, the Himalaya predecessor {5} + + https://taskwarrior.org {1} + https://taskwarrior.org/docs/timewarrior {2} + https://github.com/blindFS/vim-taskwarrior {3} + https://github.com/vimwiki/vimwiki {4} + https://github.com/soywod/kronos.vim {5} + +============================================================================== +vim:tw=78:ts=4:ft=help:norl: diff --git a/vim/ftplugin/himalaya-msg-list.vim b/vim/ftplugin/himalaya-msg-list.vim new file mode 100644 index 0000000..d4a4f5b --- /dev/null +++ b/vim/ftplugin/himalaya-msg-list.vim @@ -0,0 +1,21 @@ +setlocal buftype=nofile +setlocal cursorline +setlocal nomodifiable +setlocal nowrap +setlocal startofline + +nnoremap q :bwipeout +nnoremap :bwipeout +nnoremap :bwipeout + +call himalaya#shared#define_bindings([ + \["n", "gm" , "mbox#input" ], + \["n", "gp" , "mbox#prev_page" ], + \["n", "gn" , "mbox#next_page" ], + \["n", "", "msg#read" ], + \["n", "gw" , "msg#write" ], + \["n", "gr" , "msg#reply" ], + \["n", "gR" , "msg#reply_all" ], + \["n", "gf" , "msg#forward" ], + \["n", "ga" , "msg#attachments"], +\]) diff --git a/vim/ftplugin/himalaya-msg-read.vim b/vim/ftplugin/himalaya-msg-read.vim new file mode 100644 index 0000000..9dda7f6 --- /dev/null +++ b/vim/ftplugin/himalaya-msg-read.vim @@ -0,0 +1,18 @@ +setlocal bufhidden=wipe +setlocal buftype=nofile +setlocal cursorline +setlocal foldexpr=himalaya#shared#thread_fold(v:lnum) +setlocal foldlevel=0 +setlocal foldlevelstart=0 +setlocal foldmethod=expr +setlocal nomodifiable +setlocal nowrap +setlocal startofline + +call himalaya#shared#define_bindings([ + \["n", "gw", "msg#write" ], + \["n", "gr", "msg#reply" ], + \["n", "gR", "msg#reply_all" ], + \["n", "gf", "msg#forward" ], + \["n", "ga", "msg#attachments"], +\]) diff --git a/vim/ftplugin/himalaya-msg-write.vim b/vim/ftplugin/himalaya-msg-write.vim new file mode 100644 index 0000000..ab4c1ec --- /dev/null +++ b/vim/ftplugin/himalaya-msg-write.vim @@ -0,0 +1,13 @@ +setlocal cursorline +setlocal foldexpr=himalaya#shared#thread_fold(v:lnum) +setlocal foldlevel=0 +setlocal foldlevelstart=0 +setlocal foldmethod=expr +setlocal nowrap +setlocal startofline + +augroup himalaya + autocmd! * + autocmd BufWriteCmd call himalaya#msg#draft_save() + autocmd BufUnload call himalaya#msg#draft_handle() +augroup end diff --git a/vim/plugin/himalaya.vim b/vim/plugin/himalaya.vim new file mode 100644 index 0000000..1f88db9 --- /dev/null +++ b/vim/plugin/himalaya.vim @@ -0,0 +1,11 @@ +if exists("g:himalaya_loaded") + finish +endif + +let g:himalaya_loaded = 1 + +if !executable("himalaya") + throw "Himalaya CLI not found, see https://github.com/soywod/himalaya#installation" +endif + +command! Himalaya call himalaya#msg#list() diff --git a/vim/syntax/himalaya-msg-list.vim b/vim/syntax/himalaya-msg-list.vim new file mode 100644 index 0000000..a4ce7dc --- /dev/null +++ b/vim/syntax/himalaya-msg-list.vim @@ -0,0 +1,24 @@ +if exists("b:current_syntax") + finish +endif + +syntax match hya_sep /|/ +syntax match hya_uid /^|.\{-}|/ contains=hya_sep +syntax match hya_flags /^|.\{-}|.\{-}|/ contains=hya_uid,hya_sep +syntax match hya_subject /^|.\{-}|.\{-}|.\{-}|/ contains=hya_uid,hya_flags,hya_sep +syntax match hya_sender /^|.\{-}|.\{-}|.\{-}|.\{-}|/ contains=hya_uid,hya_flags,hya_subject,hya_sep +syntax match hya_date /^|.\{-}|.\{-}|.\{-}|.\{-}|.\{-}|/ contains=hya_uid,hya_flags,hya_subject,hya_sender,hya_sep +syntax match hya_head /.*\%1l/ contains=hya_sep +syntax match hya_unseen /^|.\{-}|šŸŸ“.*$/ contains=hya_sep + +highlight default link hya_sep VertSplit +highlight default link hya_uid Identifier +highlight default link hya_flags Special +highlight default link hya_subject String +highlight default link hya_sender Structure +highlight default link hya_date Constant + +highlight hya_head term=bold,underline cterm=bold,underline gui=bold,underline +highlight hya_unseen term=bold cterm=bold gui=bold + +let b:current_syntax = "himalaya-msg-list"