├── 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, '<\([^ >]\+\)\%([^>]*\)>\(\_[^<]*\)\1>', '\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 |
--------------------------------------------------------------------------------