├── example ├── vars │ ├── current.sh │ └── local.sh ├── github │ ├── get-repo-detail.sh │ ├── get-repo-license.sh │ └── get-profile-with-oauth.sh └── libs │ └── oauth.sh ├── .gitignore ├── .editorconfig ├── LICENSE ├── README.md ├── doc └── nvim-runscript.txt └── lua └── nvim-runscript.lua /example/vars/current.sh: -------------------------------------------------------------------------------- 1 | local.sh -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.result/ 2 | *.secret.sh 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.lua] 2 | indent_style = space 3 | indent_size = 4 4 | 5 | -------------------------------------------------------------------------------- /example/github/get-repo-detail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | . "$(dirname $0)/../vars/current.sh" 5 | 6 | # request github api and format the output with jq 7 | curl -sv "$GITHUB_ENDPOINT/repos/klesh/nvim-runscript" | jq 8 | -------------------------------------------------------------------------------- /example/vars/local.sh: -------------------------------------------------------------------------------- 1 | GITHUB_ENDPOINT=https://api.github.com 2 | GITHUB_OAUTH_LOGIN_URI=https://github.com/login/device/code 3 | GITHUB_OAUTH_TOKEN_URI=https://github.com/login/oauth/access_token 4 | GITHUB_OAUTH_GRANT_TYPE=urn:ietf:params:oauth:grant-type:device_code 5 | -------------------------------------------------------------------------------- /example/github/get-repo-license.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=$(dirname $0) 4 | . "$DIR/../vars/current.sh" 5 | 6 | license_url=$($DIR/get-repo-detail.sh "$@" | jq -r '.license.url' ) 7 | 8 | # request github api and format the output with jq 9 | curl -sv "$license_url" | jq 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Klesh Wong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/github/get-profile-with-oauth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This example shows you a breif idea of how to do OAuth API testing 4 | 5 | # First, create the OAuth app. For Github, check the following guide: 6 | # https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app 7 | # (Remember to "Enable Device Flow") 8 | 9 | # Second, we source all Variables that we need, let the `current.sh` be a symlink is recommended so we may switch 10 | # to a different set of Variables easily. 11 | . "$(dirname $0)/../vars/current.sh" 12 | 13 | # Third, source the OAuth functions file, you may copy it to whereever you like and change in anyway you want. 14 | OAUTH_SCRIPT_PATH="$(dirname $0)/../libs/oauth.sh" 15 | 16 | # It would be safer to keep your secrets in a GitIgnored file 17 | . "$(dirname $0)/../vars/my.secret.sh" 18 | # GITHUB_OAUTH_CLIENT_ID= 19 | 20 | # Finally, utilize the OAuth functions to get your access token. you may need to 21 | TOKEN=$("$OAUTH_SCRIPT_PATH" \ 22 | "$GITHUB_OAUTH_LOGIN_URI" \ 23 | "$GITHUB_OAUTH_TOKEN_URI" \ 24 | "$GITHUB_OAUTH_CLIENT_ID" \ 25 | "$GITHUB_OAUTH_GRANT_TYPE") 26 | curl -sv "$GITHUB_ENDPOINT/user" \ 27 | -H "Authorization: Bearer $TOKEN" 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-runscript 2 | 3 | Neovim users, you may not need Postman, `nvim-runscript` is the lightweight plugin you may need for API observation, 4 | testing, and debugging in a Unix-Philosophy way. 5 | 6 | ![nvim-runscript-demo](https://user-images.githubusercontent.com/61080/180638392-bc5fcb73-fe54-4af5-a256-926dfaf5a766.gif) 7 | 8 | 9 | ## Why 10 | 11 | - Lightweight and fast 12 | - The request scripts can be written in any language as long as they are executable 13 | - The request scripts can be added to Git repository and shared between teammates 14 | - You may run them in pure CLI without neovim for debugging purposes 15 | - It is easy to manipulate the response with tool that you like, and write complicated script 16 | - [Fetch author detail of the github repository](example/github/get-repo-license.sh) 17 | - [Fetch user profile with OAuth token](example/github/get-profile-with-oauth.sh) 18 | 19 | 20 | ## Requirement 21 | 22 | Developed and tested it on neovim v0.7 23 | 24 | 25 | ## Install 26 | 27 | 28 | Install with packer: 29 | ```lua 30 | use { 31 | "klesh/nvim-runscript", 32 | config = function() require("nvim-runscript").setup{} end 33 | } 34 | ``` 35 | 36 | ## How to use 37 | 38 | 1. Open a executable script file, i.e. `example/github/get-repo-detail.sh`. 39 | 2. Run commands `:RunScript`. 40 | 1. A RESULT buffer should be appear on the bottom. 41 | 2. The output of the process should be piped to the RESULT buffer. 42 | 3. A markdown file wil be saved into `example/github/get-repo-detail.sh.result/`. 43 | 3. You may re-run the script from RESULT buffer. 44 | 45 | -------------------------------------------------------------------------------- /doc/nvim-runscript.txt: -------------------------------------------------------------------------------- 1 | *nvim-runscript.txt* NeoVim users, you may not need Postman 2 | 3 | Author: Klesh Wong 4 | 5 | ============================================================================== 6 | CONTENTS *nvim-runscript* 7 | 8 | 1. Introduction |nvim-runscript-introduction| 9 | 2. Quickstart |nvim-runscript-quickstart| 10 | 3. Commands |nvim-runscript-commands| 11 | 4. Setup/Configuration |nvim-runscript-setup| 12 | 5. Mappings |nvim-runscript-mappings| 13 | 14 | ============================================================================== 15 | 1. INTRODUCTION *nvim-runscript-introduction* 16 | 17 | Runs current script file with shell, capture and save the standard input/error in markdown format. 18 | 19 | 20 | ============================================================================== 21 | 2. QUICK START *nvim-runscript-quickstart* 22 | 23 | Setup should be run in a lua file or in a |lua-heredoc| if using in a vim file. 24 | 25 | 26 | ============================================================================== 27 | 3. COMMANDS *nvim-runscript-commands* 28 | 29 | |:RunScript| 30 | 31 | Runs current script file with shell, capture and save the standard input/error in markdown format. 32 | 33 | 34 | ============================================================================== 35 | 4. SETUP *nvim-runscript-setup* 36 | 37 | You must run setup() function to initialise nvim-runscript. 38 | 39 | setup() function currently takes no argument. 40 | 41 | 42 | ============================================================================== 43 | 5. MAPPINGS *nvim-runscript-mappings* 44 | 45 | nvim-runscript doesn't provide any mappings by default, you may define your own mapping by 46 | 47 | lua 48 | > 49 | vim.api.nvim_set_keymap('n', 'rs', ':RunScript', { noremap = true }) 50 | < 51 | 52 | vimscript 53 | > 54 | :nnoremap rs :RunScript 55 | < 56 | 57 | -------------------------------------------------------------------------------- /example/libs/oauth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # OAUTH device mode 4 | 5 | set -e 6 | 7 | # process arguments 8 | LOGIN_URL=$1 9 | TOKEN_URL=$2 10 | CLIENT_ID=$3 11 | GRANT_TYPE=$4 12 | 13 | # verify arguments 14 | if [ -z "$LOGIN_URL" ] || [ -z "$CLIENT_ID" ]; then 15 | echo "Usage: $0 " >&2 16 | exit 1 17 | fi 18 | 19 | # generate variables 20 | FILENAME=$(echo $LOGIN_URL | grep -oP 'https://\K(.*?)(?=/)') 21 | LOGIN_JSON_PATH=/tmp/$FILENAME.login.json 22 | TOKEN_JSON_PATH=/tmp/$FILENAME.token.json 23 | OPEN=xdg-open 24 | # mac os 25 | if command -v open; then 26 | OPEN=open 27 | # wsl? 28 | elif command -v start; then 29 | OPEN=start 30 | fi 31 | BROWSER=${BROWSER-$OPEN} 32 | # echo "LOGIN_URL: $LOGIN_URL" 33 | # echo "TOKEN_URL: $TOKEN_URL" 34 | # echo "CLIENT_ID: $CLIENT_ID" 35 | # echo "GRANT_TYPE: $GRANT_TYPE" 36 | # echo "FILENAME: $FILENAME" 37 | # echo "LOGIN_JSON_PATH: $LOGIN_JSON_PATH" 38 | # echo "TOKEN_JSON_PATH: $TOKEN_JSON_PATH" 39 | # echo "BROWSER: $BROWSER" 40 | 41 | fetch_login_json() { 42 | curl -s "$LOGIN_URL" \ 43 | -H "content-type: application/json" \ 44 | -H "accept: application/json" \ 45 | --data @- < $LOGIN_JSON_PATH && echo OAUTH: fetch login json successfully >&2 46 | { 47 | "client_id": "$CLIENT_ID" 48 | } 49 | JSON 50 | # Step 1: App requests the device and user verification codes from GitHub 51 | # response 52 | # { 53 | # "device_code": "3584d83530557fdd1f46af8289938c8ef79f9dc5", 54 | # "user_code": "WDJB-MJHT", 55 | # "verification_uri": "https://github.com/login/device", 56 | # "expires_in": 900, 57 | # "interval": 5 58 | # } 59 | } 60 | 61 | prompt_user_code() { 62 | USER_CODE=$(jq -r '.user_code' "$LOGIN_JSON_PATH") 63 | VERIFICATION_URI="$(jq -r '.verification_uri' "$LOGIN_JSON_PATH")" 64 | echo "OAuth: Please enter the following code on the popping up page:" >&2 65 | echo "OAuth: $USER_CODE" >&2 66 | echo "OAuth: " >&2 67 | echo "OAuth: If the page wasn't popped up, enter the following URL manually in your browser:" >&2 68 | echo "OAuth: $VERIFICATION_URI" >&2 69 | echo "OAuth: " >&2 70 | echo "OAuth: This script will wait 20 seconds and then try to fetch the AccessToken every 10 seconds, be patient" >&2 71 | sleep 3 72 | "$BROWSER" "$VERIFICATION_URI" >&2 73 | sleep 17 74 | } 75 | 76 | fetch_access_token() { 77 | DEVICE_CODE="$(jq -r '.device_code' "$LOGIN_JSON_PATH" 2>/dev/null)" 78 | echo "OAUTH: try to fetch access token" >&2 79 | curl -s "$TOKEN_URL" \ 80 | -H "content-type: application/json" \ 81 | -H "accept: application/json" \ 82 | --data @- < $TOKEN_JSON_PATH 83 | { 84 | "client_id": "$CLIENT_ID", 85 | "device_code": "$DEVICE_CODE", 86 | "grant_type": "$GRANT_TYPE" 87 | } 88 | JSON 89 | # { 90 | # "access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a", 91 | # "token_type": "bearer", 92 | # "scope": "repo,gist" 93 | # } 94 | TOKEN=$(jq -r '.access_token' "$TOKEN_JSON_PATH" 2>/dev/null) 95 | [ -n "$TOKEN" ] && ! [ "$TOKEN" = "null" ] && echo OAUTH: access token fetched successfully >&2 96 | } 97 | 98 | is_login_json_valid() { 99 | if [ -f "$LOGIN_JSON_PATH" ]; then 100 | EXPIRED_IN=$(jq -r '.expires_in' "$LOGIN_JSON_PATH" 2>/dev/null) 101 | if [ "$EXPIRED_IN" -gt 0 ]; then 102 | LOGIN_TS=$(date -r "$LOGIN_JSON_PATH" "+%s") 103 | EXPIRED_TS=$(echo "$LOGIN_TS+$EXPIRED_IN-120" | bc) 104 | NOW_TS=$(date "+%s") 105 | if [ "$NOW_TS" -lt "$EXPIRED_TS" ]; then 106 | # if [ -f "$TOKEN_JSON_PATH" ]; then 107 | # TOKEN_TS=$(date -r "$TOKEN_JSON_PATH" "+%s") 108 | # if [ "$TOKEN_TS" -lt "$LOGIN_TS" ]; then 109 | # rm "$TOKEN_JSON_PATH" 110 | # fi 111 | # elif grep -qF "error" "$TOKEN_JSON_PATH"; then 112 | # rm "$LOGIN_JSON_PATH" 113 | # return 1 114 | # fi 115 | return 0 116 | fi 117 | fi 118 | fi 119 | return 1 120 | } 121 | 122 | 123 | # fetch_access_token 124 | # load_access_token 125 | TIMEOUT=$(echo $(date '+%s')+90 | bc) 126 | while [ "$(date '+%s')" -lt "$TIMEOUT" ]; do 127 | if ! is_login_json_valid ;then 128 | fetch_login_json 129 | prompt_user_code 130 | fi 131 | if fetch_access_token; then 132 | break 133 | else 134 | sleep 10 135 | fi 136 | done 137 | echo "$TOKEN" 138 | -------------------------------------------------------------------------------- /lua/nvim-runscript.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local uv = vim.loop 3 | 4 | 5 | local M = { 6 | BUFFER_OPTIONS = { 7 | swapfile = false, 8 | -- buftype = "nofile", 9 | modifiable = false, 10 | filetype = "markdown", 11 | bufhidden = "wipe", 12 | buflisted = false, 13 | } 14 | } 15 | 16 | function M.arrange_wins(script_path, result_path) 17 | vim.cmd("wincmd k") 18 | vim.cmd("e " .. vim.fn.fnameescape(script_path)) 19 | local winnr_up = api.nvim_get_current_win() 20 | vim.cmd("99wincmd j") 21 | local winnr = api.nvim_get_current_win() 22 | if winnr_up == winnr then 23 | vim.cmd("sp") 24 | vim.cmd("wincmd j") 25 | end 26 | vim.cmd("e " .. vim.fn.fnameescape(result_path)) 27 | winnr = api.nvim_get_current_win() 28 | local bufnr = api.nvim_get_current_buf() 29 | for option, value in pairs(M.BUFFER_OPTIONS) do 30 | vim.bo[bufnr][option] = value 31 | end 32 | local height = math.floor(vim.o.lines * 0.7) 33 | if api.nvim_win_get_height(winnr) ~= height then 34 | api.nvim_win_set_height(winnr, height) 35 | end 36 | return winnr, bufnr 37 | end 38 | 39 | function M.run_script(script_path) 40 | -- process arguments 41 | if not script_path or script_path == "" then 42 | script_path = vim.fn.expand("%:p") 43 | end 44 | local suffix = ".result/" .. os.date('%Y-%m-%dT%H-%M-%S') .. ".md" 45 | if script_path:match("%.result/%d%d%d%d%-%d%d%-%d%dT%d%d%-%d%d%-%d%d%.md$") then 46 | script_path = script_path:sub(0, - #suffix - 1) 47 | end 48 | local result_path = script_path .. suffix 49 | -- make sure result folder exists 50 | os.execute("mkdir -p '" .. script_path .. ".result'") 51 | -- arrange windows for viewing sciprt / result 52 | local winnr, bufnr = M.arrange_wins(script_path, result_path) 53 | 54 | 55 | local stdin = uv.new_pipe(false) 56 | local stdout = uv.new_pipe(false) 57 | local stderr = uv.new_pipe(false) 58 | 59 | local output_buf = { 60 | stdout = '', 61 | stderr = '', 62 | all = '', 63 | } 64 | local function update_buf(lines, move_to_line) 65 | api.nvim_buf_set_option(bufnr, "modifiable", true) 66 | api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) 67 | api.nvim_buf_set_option(bufnr, "modifiable", false) 68 | api.nvim_buf_set_option(bufnr, "modified", false) 69 | if api.nvim_win_is_valid(winnr) then 70 | api.nvim_win_set_cursor(winnr, { move_to_line, 0 }) 71 | end 72 | end 73 | 74 | local function update_chunk(key, chunk) 75 | if chunk then 76 | output_buf[key] = output_buf[key] .. chunk 77 | output_buf.all = output_buf.all .. chunk 78 | local lines = vim.split(output_buf.all, '\n', true) 79 | update_buf(lines, #lines) 80 | end 81 | end 82 | 83 | update_chunk = vim.schedule_wrap(update_chunk) 84 | 85 | local handle, pid, started_at 86 | started_at = os.time() 87 | handle, pid = uv.spawn("sh", { 88 | stdio = { stdin, stdout, stderr }; 89 | -- cwd = cwd; 90 | }, function(code, signal) 91 | stdin:close() 92 | stdout:close() 93 | stderr:close() 94 | handle:close() 95 | 96 | vim.schedule(function() 97 | local stdout_lines = vim.split(output_buf.stdout, '\n', true) 98 | local stderr_lines = vim.split(output_buf.stderr, '\n', true) 99 | 100 | local stdout_fmt = "json" 101 | for _, line in ipairs(stderr_lines) do 102 | local fmt = line:lower():match '^.*%s*content%-type%p.*(json)' 103 | if not fmt then 104 | fmt = line:lower():match '^.*%s*content%-type%p.*(xml)' 105 | end 106 | if not fmt then 107 | fmt = line:lower():match '^.*%s*content%-type%p.*(yml)' 108 | end 109 | if not fmt then 110 | fmt = line:lower():match '^.*%s*content%-type%p.*(yaml)' 111 | end 112 | if fmt then 113 | stdout_fmt = fmt 114 | end 115 | end 116 | 117 | local lines = vim.tbl_flatten { 118 | "stderr:", 119 | "```sh", 120 | stderr_lines, 121 | "```", 122 | "", 123 | "Total Elapsed Time: " .. os.difftime(os.time(), started_at) .. "s", 124 | "Exit Code:" .. code .. " Signal: " .. signal, 125 | "", 126 | "stdout:", 127 | "```" .. stdout_fmt, 128 | stdout_lines, 129 | "```", 130 | } 131 | update_buf(lines, #stderr_lines + 7) 132 | api.nvim_buf_call(bufnr, function() 133 | vim.cmd "w" 134 | vim.cmd "e" 135 | end) 136 | end) 137 | end) 138 | 139 | update_buf({ string.format("Started %s PID: %d", script_path, pid) }, 1) 140 | 141 | -- If the buffer closes, then kill our process. 142 | api.nvim_buf_attach(bufnr, false, { 143 | on_detach = function() 144 | if not handle:is_closing() then 145 | handle:kill(15) 146 | end 147 | end; 148 | }) 149 | 150 | stdout:read_start(function(_, chunk) update_chunk("stdout", chunk) end) 151 | stderr:read_start(function(_, chunk) update_chunk("stderr", chunk) end) 152 | stdin:write(script_path) 153 | stdin:write("\n") 154 | stdin:shutdown() 155 | end 156 | 157 | function M.setup() 158 | vim.api.nvim_create_user_command("RunScript", function(res) 159 | M.run_script(res.args) 160 | end, { nargs = "?" }) 161 | end 162 | 163 | return M 164 | --------------------------------------------------------------------------------