├── README.md ├── autoload └── mastodon.vim └── plugin └── mastodon.vim /README.md: -------------------------------------------------------------------------------- 1 | # vim-mastodon 2 | 3 | Mastodon client for Vim 4 | 5 | Currently, work in progress! 6 | 7 | ## Usage 8 | 9 | ### toot 10 | 11 | ``` 12 | :Mastodon toot Blah! 13 | ``` 14 | 15 | ### timeline 16 | 17 | ``` 18 | :Mastodon timeline 19 | ``` 20 | 21 | ### stream 22 | 23 | ``` 24 | :Mastodon stream 25 | ``` 26 | 27 | ## Configuration (WIP) 28 | 29 | ``` 30 | let g:mastodon_host = 'mstdn.jp' 31 | let g:mastodon_access_token = 'XXXXXXX' 32 | ``` 33 | 34 | ## Requirements 35 | 36 | [webapi-vim](https://github.com/mattn/webapi-vim) 37 | 38 | ## License 39 | 40 | MIT 41 | 42 | ## Author 43 | 44 | Yasuhiro Matsumoto (a.k.a. mattn) 45 | -------------------------------------------------------------------------------- /autoload/mastodon.vim: -------------------------------------------------------------------------------- 1 | let s:URL_PROTOCOL = '\%([Hh][Tt][Tt][Pp]\|[Hh][Tt][Tt][Pp][Ss]\|[Ff][Tt][Pp]\)://' 2 | let s:URL_PROTOCOL_HTTPS = '\%([Hh][Tt][Tt][Pp][Ss]\)://' 3 | let s:URL_PROTOCOL_NON_HTTPS = '\%([Hh][Tt][Tt][Pp]\|[Ff][Tt][Pp]\)://' 4 | let s:URL_DOMAIN_CHARS = '[a-zA-Z0-9!$&''()*+,.:;=?@_~%#-]' 5 | let s:URL_DOMAIN_END_CHARS = '[a-zA-Z0-9!$&''*+=?@_~%#-]' 6 | let s:URL_DOMAIN_PARENS = '('.s:URL_DOMAIN_CHARS.'*)' 7 | let s:URL_DOMAIN = '\%('.'\%('.s:URL_DOMAIN_CHARS.'*'.s:URL_DOMAIN_PARENS.'\)'.'\|'.'\%('.s:URL_DOMAIN_CHARS.'*'.s:URL_DOMAIN_END_CHARS.'\)'.'\)' 8 | let s:URL_PATH_CHARS = '[a-zA-Z0-9!$&''()*+,./:;=?@_~%#-]' 9 | let s:URL_PARENS = '('.s:URL_PATH_CHARS.'*)' 10 | let s:URL_PATH_END_CHARS = '[a-zA-Z0-9!$&''*+/=?@_~%#-]' 11 | let s:URL_PATH = '\%('.'\%('.s:URL_PATH_CHARS.'*'.s:URL_PARENS.'\)'.'\|'.'\%('.s:URL_PATH_CHARS.'*'.s:URL_PATH_END_CHARS.'\)'.'\)' 12 | let s:URLMATCH = s:URL_PROTOCOL.s:URL_DOMAIN.'\%(/'.s:URL_PATH.'\=\)\=' 13 | let s:URLMATCH_HTTPS = s:URL_PROTOCOL_HTTPS.s:URL_DOMAIN.'\%(/'.s:URL_PATH.'\=\)\=' 14 | let s:URLMATCH_NON_HTTPS = s:URL_PROTOCOL_NON_HTTPS.s:URL_DOMAIN.'\%(/'.s:URL_PATH.'\=\)\=' 15 | 16 | function! s:to_text(str) 17 | let str = a:str 18 | let str = substitute(str, '', "\n", 'g') 19 | let str = substitute(str, '<\([^/]\+\)/>', '', 'g') 20 | while 1 21 | let tmp = substitute(str, '<\([^ >]\+\)\%([^>]*\)>\(\_[^<]*\)', '\2', 'g') 22 | if tmp == str 23 | break 24 | endif 25 | let str = tmp 26 | endwhile 27 | let str = substitute(str, '>', '>', 'g') 28 | let str = substitute(str, '<', '<', 'g') 29 | let str = substitute(str, '"', '"', 'g') 30 | let str = substitute(str, ''', "'", 'g') 31 | let str = substitute(str, ' ', ' ', 'g') 32 | let str = substitute(str, '¥', '\¥', 'g') 33 | let str = substitute(str, '&#\(\d\+\);', '\=s:nr2enc_char(submatch(1))', 'g') 34 | let str = substitute(str, '&', '\&', 'g') 35 | let str = substitute(str, '»', '>', 'g') 36 | let str = substitute(str, '«', '<', 'g') 37 | return str 38 | endfunction 39 | 40 | function! s:format(item) 41 | if !empty(a:item.reblog) 42 | return a:item.account.acct . ': BOOST: ' . s:to_text(a:item.reblog.content) 43 | endif 44 | return a:item.account.acct . ': ' . s:to_text(a:item.content) 45 | endfunction 46 | 47 | function! s:append_line(expr, text) abort 48 | if bufnr(a:expr) == -1 49 | return 50 | endif 51 | let mode = mode() 52 | let oldnr = winnr() 53 | let winnr = bufwinnr(a:expr) 54 | if oldnr != winnr 55 | if winnr == -1 56 | silent exec "sp ".escape(bufname(bufnr(a:expr)), ' \') 57 | else 58 | exec winnr.'wincmd w' 59 | endif 60 | endif 61 | setlocal modifiable | call append('$', a:text) | setlocal nomodifiable 62 | let pos = getpos('.') 63 | let pos[1] = line('$') 64 | let pos[2] = 9999 65 | call setpos('.', pos) 66 | if oldnr != winnr 67 | if winnr == -1 68 | silent hide 69 | endif 70 | endif 71 | 72 | exec oldnr.'wincmd w' 73 | if mode =~# '[sSvV]' 74 | silent! normal gv 75 | endif 76 | if mode !~# '[cC]' 77 | redraw 78 | endif 79 | endfunction 80 | 81 | function! s:show_timeline(items) 82 | let winnum = bufwinnr(bufnr('mastodon://')) 83 | if winnum != -1 84 | if winnum != bufwinnr('%') 85 | exe winnum 'wincmd w' 86 | endif 87 | setlocal modifiable 88 | else 89 | silent noautocmd rightbelow new 90 | setlocal noswapfile 91 | silent exec 'noautocmd file' 'mastodon://' 92 | endif 93 | 94 | let old_undolevels = &undolevels 95 | set undolevels=-1 96 | filetype detect 97 | silent %d _ 98 | call setline(1, map(a:items, 's:format(v:val)')) 99 | let &undolevels = old_undolevels 100 | setlocal buftype=acwrite bufhidden=hide noswapfile 101 | setlocal bufhidden=wipe 102 | setlocal nomodified 103 | setlocal nomodifiable 104 | 105 | syntax clear 106 | syntax match mastodonUser /^.\{-1,}:/ 107 | syntax match mastodonTime /|[^|]\+|$/ contains=mastodonTimeBar 108 | syntax match mastodonTimeBar /|/ contained 109 | execute 'syntax match mastodonLink "\<'.s:URLMATCH.'"' 110 | syntax match mastodonReply "\w\@ 2 128 | return [] 129 | endif 130 | return filter(['toot', 'toot-buffer', 'timeline', 'stream'], 'stridx(v:val, a:alead)>=0') 131 | endfunction 132 | 133 | function! mastodon#call(...) 134 | let host = get(g:, 'mastodon_host') 135 | let access_token = get(g:, 'mastodon_access_token') 136 | let auth_header = {'Authorization': 'Bearer ' . access_token} 137 | 138 | let method = get(a:000, 0, '') 139 | let args = get(a:000, 1, '') 140 | if method == 'timeline' 141 | let res = webapi#http#get( 142 | \ printf('https://%s/api/v1/timelines/home', host), 143 | \ {}, 144 | \ auth_header) 145 | if res.status != 200 146 | return res.message 147 | endif 148 | let items = webapi#json#decode(res.content) 149 | call s:show_timeline(items) 150 | elseif method == 'toot' 151 | let text = join(a:000[1:], " ") 152 | let res = webapi#http#post( 153 | \ printf('https://%s/api/v1/statuses', host), 154 | \ {'status': text}, 155 | \ auth_header) 156 | if res.status != 200 157 | return res.message 158 | endif 159 | elseif method == 'toot-buffer' 160 | let text = join(a:000[1:], "\n") 161 | let res = webapi#http#post( 162 | \ printf('https://%s/api/v1/statuses', host), 163 | \ {'status': text}, 164 | \ auth_header) 165 | if res.status != 200 166 | return res.message 167 | endif 168 | elseif method == 'stream' 169 | call s:show_timeline([]) 170 | call webapi#http#stream({ 171 | \ 'url': printf('https://%s/api/v1/streaming/public/local', host), 172 | \ 'header': auth_header, 173 | \ 'out_cb': function('mastodon#add_item'), 174 | \}) 175 | endif 176 | endfunction 177 | 178 | let s:lastline = '' 179 | function! mastodon#add_item(data) 180 | let data = a:data 181 | if data =~ '^event:' 182 | let s:lastline = substitute(data, '^event:\s*\(\w\+\)', '\1', '') 183 | elseif data =~ '^data:' && s:lastline == 'update' 184 | let data = substitute(data, '^data:\s*', '', '') 185 | let item = webapi#json#decode(data) 186 | call s:append_line('mastodon://', s:format(item)) 187 | endif 188 | endfunction 189 | -------------------------------------------------------------------------------- /plugin/mastodon.vim: -------------------------------------------------------------------------------- 1 | command! -nargs=+ -complete=customlist,mastodon#complete Mastodon call mastodon#call() 2 | --------------------------------------------------------------------------------