mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-22 02:50:19 +00:00
import vim plugin
This commit is contained in:
parent
cbc74916a9
commit
e23e363e0a
14 changed files with 1120 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/target
|
||||
/vim/doc/tags
|
||||
|
|
125
vim/README.md
Normal file
125
vim/README.md
Normal file
|
@ -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 | `<Enter>` |
|
||||
| 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 <plug>(himalaya-mbox-input)
|
||||
nmap gp <plug>(himalaya-mbox-prev-page)
|
||||
nmap gn <plug>(himalaya-mbox-next-page)
|
||||
nmap <cr> <plug>(himalaya-msg-read)
|
||||
nmap gw <plug>(himalaya-msg-write)
|
||||
nmap gr <plug>(himalaya-msg-reply)
|
||||
nmap gR <plug>(himalaya-msg-reply-all)
|
||||
nmap gf <plug>(himalaya-msg-forward)
|
||||
nmap ga <plug>(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 <plug>(himalaya-msg-write)
|
||||
nmap gr <plug>(himalaya-msg-reply)
|
||||
nmap gR <plug>(himalaya-msg-reply-all)
|
||||
nmap gf <plug>(himalaya-msg-forward)
|
||||
nmap ga <plug>(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
|
57
vim/autoload/himalaya/mbox.vim
Normal file
57
vim/autoload/himalaya/mbox.vim
Normal file
|
@ -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
|
107
vim/autoload/himalaya/mbx.vim
Normal file
107
vim/autoload/himalaya/mbx.vim
Normal file
|
@ -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
|
241
vim/autoload/himalaya/msg.vim
Normal file
241
vim/autoload/himalaya/msg.vim
Normal file
|
@ -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
|
28
vim/autoload/himalaya/shared.vim
Normal file
28
vim/autoload/himalaya/shared.vim
Normal file
|
@ -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("<plug>(himalaya-%s)", plug)
|
||||
execute printf("%snoremap <silent>%s :call himalaya#%s()<cr>", mode, plug, name)
|
||||
|
||||
if !hasmapto(plug, mode)
|
||||
execute printf("%smap <nowait><buffer>%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
|
238
vim/autoload/himalaya/ui.vim
Normal file
238
vim/autoload/himalaya/ui.vim
Normal file
|
@ -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
|
87
vim/autoload/himalaya/utils.vim
Normal file
87
vim/autoload/himalaya/utils.vim
Normal file
|
@ -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
|
149
vim/doc/himalaya.txt
Normal file
149
vim/doc/himalaya.txt
Normal file
|
@ -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 <CR>
|
||||
Show task infos {K}
|
||||
Set context {gc}
|
||||
Show worktime {gw}
|
||||
Jump to the next cell <C-n>
|
||||
Jump to the prev cell <C-p>
|
||||
Delete in cell {dic}
|
||||
Change in cell {cic}
|
||||
Visual in cell {vic}
|
||||
|
||||
You can customize them:
|
||||
>
|
||||
nmap gd <plug>(himalaya-list-done)
|
||||
nmap gD <plug>(himalaya-list-deleted)
|
||||
nmap <cr> <plug>(himalaya-toggle)
|
||||
nmap K <plug>(himalaya-info)
|
||||
nmap gc <plug>(himalaya-context)
|
||||
nmap gw <plug>(himalaya-worktime)
|
||||
nmap <c-n> <plug>(himalaya-next-cell)
|
||||
nmap <c-p> <plug>(himalaya-prev-cell)
|
||||
nmap dic <plug>(himalaya-delete-in-cell)
|
||||
nmap cic <plug>(himalaya-change-in-cell)
|
||||
nmap vic <plug>(himalaya-visual-in-cell)
|
||||
<
|
||||
==============================================================================
|
||||
LICENSE *himalaya-license*
|
||||
|
||||
Copyright © 2019-2020 Clément DOUIN <clement.douin@posteo.net>
|
||||
|
||||
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:
|
21
vim/ftplugin/himalaya-msg-list.vim
Normal file
21
vim/ftplugin/himalaya-msg-list.vim
Normal file
|
@ -0,0 +1,21 @@
|
|||
setlocal buftype=nofile
|
||||
setlocal cursorline
|
||||
setlocal nomodifiable
|
||||
setlocal nowrap
|
||||
setlocal startofline
|
||||
|
||||
nnoremap <buffer><silent>q :bwipeout<cr>
|
||||
nnoremap <buffer><silent><cr> :bwipeout<cr>
|
||||
nnoremap <buffer><silent><esc> :bwipeout<cr>
|
||||
|
||||
call himalaya#shared#define_bindings([
|
||||
\["n", "gm" , "mbox#input" ],
|
||||
\["n", "gp" , "mbox#prev_page" ],
|
||||
\["n", "gn" , "mbox#next_page" ],
|
||||
\["n", "<cr>", "msg#read" ],
|
||||
\["n", "gw" , "msg#write" ],
|
||||
\["n", "gr" , "msg#reply" ],
|
||||
\["n", "gR" , "msg#reply_all" ],
|
||||
\["n", "gf" , "msg#forward" ],
|
||||
\["n", "ga" , "msg#attachments"],
|
||||
\])
|
18
vim/ftplugin/himalaya-msg-read.vim
Normal file
18
vim/ftplugin/himalaya-msg-read.vim
Normal file
|
@ -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"],
|
||||
\])
|
13
vim/ftplugin/himalaya-msg-write.vim
Normal file
13
vim/ftplugin/himalaya-msg-write.vim
Normal file
|
@ -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! * <buffer>
|
||||
autocmd BufWriteCmd <buffer> call himalaya#msg#draft_save()
|
||||
autocmd BufUnload <buffer> call himalaya#msg#draft_handle()
|
||||
augroup end
|
11
vim/plugin/himalaya.vim
Normal file
11
vim/plugin/himalaya.vim
Normal file
|
@ -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()
|
24
vim/syntax/himalaya-msg-list.vim
Normal file
24
vim/syntax/himalaya-msg-list.vim
Normal file
|
@ -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"
|
Loading…
Reference in a new issue