Recently, I have been sharing a lot code snippets and other textual 😏 files over the
internet. So, currently I just run a curl command and then upload
the file to https://0x0.st.
The command looks like this: curl -F'file=@/absolute/path/to/file' 0x0.st | xclip
.
We will be just transforming this command into a Neovim utility.
Other similar plugins.
So, before writing this I did try several other plugins. I did not like any but, maybe
you will.
- There is paperplanes.nvim but it is
a bit too feature-rich. It supports multiple backends and other fringe features that
I do not really need.
- There is pastebin-vim which is also cumbersome
to use as you’d need to generate API keys first.
- Then there’s snips.nvim which is the best
so far. But again I want something minimal.
- Lastly, there is vim-pastebins which I have not tried.
We will be using the well-known plenary.nvim
library plugin. The only utilities we need from this are the Job
and Path
class.
Which just basically spawns
an async task in the background and calls an on_exit
callback function when
the task is done. And, the other is just the Lua version of Python’s
pathlib.
The readme has examples of Job
but here is an example anyway.
1
2
3
4
5
6
7
8
9
10
11
12
| local Job = require("plenary.job")
Job:new({
command = "ls"
args = { "--recursive" },
on_exit = vim.schedule_wrap(function(self, code, _)
if code ~= 0 then
vim.notify(vim.inspect(self:stderr_result()))
return
end
vim.notify(vim.inspect(self:result()))
end),
})
|
Alternative APIs.
Following are alternative APIs to plenary’s Job
💀.
Following are alternative APIs to plenary’s Path
😢.
A nice little exercise would be to create your version of this script but by using
these alternative APIs instead.
Here’s a list of features that I primarily want.
- Ability to upload a file.
- Ability to upload specific lines from a file.
- Ability to remove the uploaded item.
- Ability to cache uploaded file into a JSON file.
As for going about implementing the upload feature for selected text, we would need to copy
that selected text or, file and paste those contents into another temporary file and then
upload that file to the pastebin website.
We just need defaults for paths beforehand. We would need the following paths.
db_path
: Path where all previously upload request responses will be saved.tmp_path
: This is only needed for selected text upload.dump_path
: Path to where all of the response headers will be written to by CURL.
1
2
3
4
5
6
7
| local M = {}
M.config = {
db_path = vim.fn.stdpath("state") .. "/paste.db.json",
tmp_path = "/tmp/paste",
dump_path = "/tmp/dump", -- needs to match with the scripts dump path (explained later)
}
|
As, for the setup
function we now need to extend the default configuration table with the
current one (the one i.e. passed in through the setup function). We do this because
if we ever decide to change the configuration on the fly, then this will be useful and
we won’t need to reload Neovim again.
1
2
3
4
5
6
7
8
9
10
| function M.setup(options)
options = vim.F.if_nil(options, {})
M.config = vim.tbl_deep_extend("keep", options, M.config)
M._db = Path:new(M.config.db_path)
M._dump = Path:new(M.config.dump_path)
M._path = Path:new(M.config.tmp_path)
M._responses = {}
-- load db entries (persist)
if M._db:exists() then M._responses = vim.json.decode(M._db:read()) end
end
|
After this, we initialize some private tables for several caching and file IO operations.
M._response
: This table is used for storing responses. For instance a response from https://0x0.st
might look like the following.
1
2
3
4
5
6
7
8
9
10
11
12
| HTTP/2 200
server: nginx
date: Mon, 11 Mar 2024 16:06:07 GMT
content-type: text/html; charset=utf-8
content-length: 24
x-expires: 1741668317786
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-frame-options: sameorigin
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
referrer-policy: no-referrer, strict-origin-when-cross-origin
...
|
We would then parse this to a key: value pair value and then encode
these into a JSON format. See: M.pretty().
M._dump
: Path
instance of M.config.dump_path
where responses from CURL will
be written to (by CURL itself) before parsing into a key: value.
Again, we will be reading and writing to it a lot so, it’s better to make it
a pair value.
M._db
: Path
instance of M.config.db_path
.
M.tmp_path
: The path where currently selected buffer contents will be copied to.
So for this we just need to follow the following steps.
- Prepare the file
M._path
for upload by writing all the buffer contents to it. - Then upload the newly updated file to https://0x0.st using a job API.
- Save the returned link into the clipboard.
- Save the response dumped into
M._dump
to the M._responses
table. - Clean
M._path
and M._dump
. - Save all recorded
M._responses
upto this point to M._db
path.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| function M.paste(contents)
M._path:write(table.concat(contents, "\n"), "w")
Job:new({
command = "curl",
args = { -- read the manpage - I can't be fucked.
"--silent",
"--show-error",
"--location",
"--dump-header",
M._dump.filename,
"--compressed",
"--request",
"POST",
"--form",
string.format("file=@%s", M._path.filename),
"https://0x0.st",
},
on_exit = vim.schedule_wrap(function(self, code, _)
if code ~= 0 then return end
local body = self:result()[1] -- [1] because it only returns a link
local pretty = M.pretty(M._dump:read()) -- defined below
M._responses[body] = {
body = body,
headers = pretty.headers,
code = pretty.code
}
-- clean used paths (optional)
M._path:rm()
M._dump:rm()
vim.fn.setreg("+", body)
vim.api.nvim_notify("+pastebin: " .. body, vim.log.levels.INFO, {
title = "pastebin",
icon = " "
})
M._db:write(vim.json.encode(M._responses), "w")
end),
}):start()
end
|
CURL returns results in a weird format,
so next we will be writing a M.pretty
function that will convert these values into a
Lua table. Which then can be encoded to a JSON format.
You could define M.pretty
as a local function if you want.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| function M.pretty(data)
local lines = vim.split(data, "\r\n", { plain = true })
local headers = {}
local heading = vim.split(lines[1], " ", { plain = true })
local code = heading[2]
table.remove(lines, 1)
for _, line in ipairs(lines) do
local _line = vim.split(line, ": ", { plain = true })
if #_line > 1 then
local _colon_items = vim.split(_line[2], "; ", { plain = true })
if #_colon_items == 1 then
headers[_line[1]] = _colon_items[1]
elseif #_colon_items > 1 then
headers[_line[1]] = _colon_items
end
end
end
return { code = code, headers = headers }
end
|
For deleting a link form https://0x0.st, we just need to send the link, that we want
to delete and its token that was given to us when we uploaded the file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| function M.delete(response)
if not response then return end
-- 0x0 will not allow deletion if we are unable to supply the token
if not response.headers["x-token"] then
M._responses[response.body] = nil -- rm from cache? This depends on your preference.
M._db:write(vim.json.encode(M._responses), "w") -- useless if you chose to rm previous line
return
end
Job:new({
command = "curl",
args = {
"--form",
string.format("token=%s", response.headers["x-token"]),
"--form",
"delete=",
response.body, -- file link to delete example: https://0x0.st/HkmZ.lua
},
on_exit = vim.schedule_wrap(function(self, code, _)
if code ~= 0 then return end
-- I use https://github.com/rcarriga/nvim-notify
-- if you don't then just use print("Deleted" .. response.body)
vim.api.nvim_notify("Deleted " .. response.body, vim.log.levels.INFO, {
title = "pastebin",
icon = " "
})
M._responses[response.body] = nil
M._db:write(vim.json.encode(M._responses), "w") -- save cache (persist)
end),
}):start()
end
|
Why though?
- Will you only ever upload code files?
- Won’t you want to upload a file without opening Neovim?
- Wouldn’t it be better if there were a nice shell utility that can be used everywhere?
- Minimalism.
So, we write a shell script that does just that. (Maybe we’ll write completions for it as well!)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| #!/usr/bin/env bash
dump="/tmp/dump"
function help_message() {
echo 'usage: paste [[--upload|-u]|[--delete|-d]|[--clean|-c]|[--help|-h]]'
echo
echo 'script to upload files to a pastebin website'
echo
echo 'options:'
echo ' -h, --help show this help message and exit'
echo ' -c, --clean remove db'
echo ' -u <FILE>, --upload <FILE> upload a file to 0x0.st'
echo ' -d <TOKEN> <LINK>, --delete <TOKEN> <LINK> delete an uploaded file'
echo
echo 'Source: https://github.com/dharmx'
}
function _upload() {
[[ "$1" == "" ]] && echo Needs '<FILE>' && help_message && return 1
curl \
--silent \
--show-error \
--location \
--dump-header "$dump" \
--compressed \
--request POST \
--form file=@"$1" \
https://0x0.st
}
function _delete() {
[[ "$1" == "" || "$2" == "" ]] && echo Needs '<TOKEN> <LINK>' && help_message && return 1
curl --form token="$1" --form delete= "$2"
}
case "$1" in
--upload|-u) _upload "$2" ;;
--delete|-d) _delete "$2" "$3" ;;
--clean|-c) rm -rf "$dump" ;;
--help|-h) help_message ;;
*) echo -e Needs arguments! "\n" && help_message && exit 1 ;;
esac
|
We then place this into $PATH
. I recommend ~/.local/bin
. Now, you can use 0x0 -u file
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #compdef 0x0
# Sing with me. WE 👏. LOVE 👏. COMPLETIONS 👏!
function _0x0() {
_arguments \
'-h[show this help message and exit]' \
'--help[show this help message and exit]' \
'-c[remove db]' \
'--clean[remove db]' \
'-u[upload a file to 0x0.st]:upload:_files' \
'--upload[upload a file to 0x0.st]:upload:_files' \
'-d[delete an uploaded file]' \
'--delete[delete an uploaded file]'
}
|
We then place this into $FPATH
. I recommend ~/.local/share/zsh/completions
.
Name the file 0x0
or, something 🤷.
Only the M.paste
and M.delete
functions need to be re-written with new arguments. See
below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| -- ...
function M.paste(contents)
M._path:write(table.concat(contents, "\n"), "w")
Job:new({
command = "0x0", -- the script's name
args = { "--upload", M._path.filename },
on_exit = vim.schedule_wrap(function(self, code, _)
if code ~= 0 then return end
local body = self:result()[1]
local pretty = M.pretty(M._dump:read())
M._responses[body] = { body = body, headers = pretty.headers, code = pretty.code }
M._path:rm()
M._dump:rm()
vim.fn.setreg("+", body)
vim.api.nvim_notify(body, vim.log.levels.INFO, { title = "0x0", icon = " " })
M._db:write(vim.json.encode(M._responses), "w")
end),
}):start()
end
function M.delete(response)
if not response then return end
if not response.headers["x-token"] then
M._responses[response.body] = nil
M._db:write(vim.json.encode(M._responses), "w")
return
end
Job:new({
command = "0x0",
args = { "--delete", response.headers["x-token"], response.body },
on_exit = vim.schedule_wrap(function(self, code, _)
if code ~= 0 then return end
vim.api.nvim_notify(vim.inspect(self:result()), vim.log.levels.INFO, { title = "0x0", icon = " " })
M._responses[response.body] = nil
M._db:write(vim.json.encode(M._responses), "w")
end),
}):start()
end
-- ...
|
Command
There are a few rules we’ll follow.
<bang>
should not allow an file path arguments.- Without
<bang>
allow file path arguments. - If no
<bang>
or, any arguments are supplied, then use current buffer.
And, only allow ranges in this case.
1
2
3
4
5
6
7
8
9
10
11
12
| -- at the very end of your paste.lua
function M.command(args)
if args.bang and args.fargs[1] then
M.delete(M._responses[args.fargs[1]])
elseif args.fargs[1] then
M.paste(vim.fn.readfile(file))
else
args.line1 = (args.range == 2 and args.line1 or 1) - 1
args.line2 = args.range == 2 and args.line2 or -1
M.paste(vim.api.nvim_buf_get_lines(0, args.line1, args.line2, false))
end
end
|
Now, you’d just import the paste
module and call the command
function and
pass in the args
parameter.
See: :help nvim_create_user_command
I have it in ~/.config/nvim/lua/scratch/paste.lua
so, I’d need to call require("scratch.paste")
.
If you have it in say, ~/.config/nvim/lua/paste.lua
then you’d just need to do require("paste")
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| -- in your init.lua
vim.api.nvim_create_user_command("Paste", function(args)
require("scratch.paste").command(args)
end, {
desc = "Upload/delete files/snippets to a pastebin site.",
range = true,
bang = true,
nargs = "?",
complete = function(arg, name, _)
if name:match("^'<,'>Paste") then return {} end
if name:match("^Paste!") then
return vim.tbl_keys(require("scratch.paste")._responses)
end
return vim.fn.getcompletion(arg, "file")
end,
})
|
The custom command completion function is not complicated at all. We just check
- If the dumbass is using ranges. If yes then do not supply any completions.
- If the restarted individual is using
<bang>
i.e. :Paste!
. If they are, then supply
all response bodies list, which the user would want to delete. - Else supply with file paths.
Please read TOS
Also, please do not forget to read the TOS. Like the maximum file
size you’re allowed to upload. Try not to upload furry
porn to it. You can use mega.io for that. And, please no gore or,
propaganda videos.
Just go and read it before you do anything stupid.
I hope you enjoyed reading this meme 💀. If you did then comment “Sex” for 25 Robux.