Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

57 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

job.nvim

job.nvim is a lightweight, high-performance async job manager for Neovim, built on top of vim.uv (libuv). It provides a simple and clean API for spawning processes, sending data, managing I/O streams, and handling timeouts β€” all without blocking the editor. Whether you need to run shell commands, interact with long-running processes, or build async pipelines, job.nvim offers a composable approach with minimal overhead.

Run Tests GitHub License GitHub Issues or Pull Requests GitHub commit activity GitHub Release luarocks

✨ Features

  • High-performance async job execution powered by vim.uv (libuv)
  • Simple yet extensible API for process management
  • Support for both string and table command formats
  • Automatic encoding conversion for non-UTF-8 output
  • Process timeout with automatic SIGTERM termination
  • Detached process mode for background tasks
  • Full control over stdin/stdout/stderr channels
  • Process PID retrieval for external management
  • Cross-platform support (Linux, macOS, Windows)

πŸ“¦ Installation

job.nvim works with all major Neovim plugin managers. Neovim 0.10+ is recommended for best compatibility.

  • Using nvim-plug

    require('plug').add({
      {
        'wsdjeg/job.nvim',
      },
    })
  • Using lazy.nvim

    {
      "wsdjeg/job.nvim",
      config = function()
        -- No configuration needed
      end,
    }
  • Using packer.nvim

    use({
      'wsdjeg/job.nvim',
    })
  • Using luarocks

    luarocks install job.nvim
    

πŸ”§ Configuration

No configuration is needed. Simply require the module and start using it:

local job = require('job')

βš™οΈ Basic Usage

Basic example:

local job = require('job')
local function on_exit(id, code, signal)
    print('job ' .. id .. ' exit code:' .. code .. ' signal:' .. signal)
end

local cmd = { 'echo', 'hello world' }
local jobid1 = job.start(cmd, {
    on_stdout = function(id, data)
        vim.print(data)
    end,
    on_exit = on_exit,
})

vim.print(string.format('jobid is %s', jobid1))

local jobid = job.start({ 'cat' }, {
    on_stdout = function(id, data)
        vim.print(data)
    end,
    on_exit = function(id, code, signal)
        print('job ' .. id .. ' exit code:' .. code .. ' signal:' .. signal)
    end,
})

job.send(jobid, { 'hello' })

job.chanclose(jobid, 'stdin')

Output:

jobid is 43
{ "hello world" }
job 43 exit code:0 signal:0
{ "hello" }
job 44 exit code:0 signal:0

Using PID

You can get the process ID (PID) of a running job for external process management:

local job = require('job')
local jobid = job.start({ 'python', 'server.py' }, {
    on_stdout = function(id, data) vim.print(data) end,
})

-- Get the process ID
local pid = job.pid(jobid)
if pid then
    print('Server running with PID:', pid)
    -- You can now use this PID with external tools or save it for later
end

Using a shell command string

local jobid = job.start('echo "hello from shell"', {
    on_stdout = function(id, data) vim.print(data) end,
})

Error handling

job.start returns a positive integer job ID on success, or one of the following error codes:

  • 0: invalid command type or empty command
  • -1: command not executable or spawn failure
  • -2: cwd option is not a directory

Example:

local id = job.start({ '' }, {})
if id <= 0 then
    vim.print('Failed to start job, error code:', id)
end

πŸ“š APIs

function description returns
job.start(cmd, opt) start a new job job id (>0) or error code (≀0)
job.stop(jobid, signal) stop the job with signal (integer) none
job.send(jobid, data) send data (string or table of strings) to job none
job.chanclose(jobid, std) close channel (stdin, stdout, or stderr) none
job.pid(jobid) get the process ID (PID) of the job PID or nil

job.start(cmd, opts)

  • cmd (string|table): command to execute. If a string, it is passed to the shell. If a table, the first element is the executable and the rest are arguments.
  • opts (table|nil): job options (see Job options).

job.stop(id, signal)

  • id (integer): job ID returned by job.start.
  • signal (integer): POSIX signal number (e.g., 9 for SIGKILL, 15 for SIGTERM).

Tip: You can also use job.pid() to get the process ID and send signals using external tools like kill.

job.send(id, data)

  • id (integer): job ID.
  • data (string|table|nil): data to send. If a table, each element is written as a line. If nil, an empty string is sent and stdin is shut down.

job.chanclose(id, t)

  • id (integer): job ID.
  • t (string): which channel to close: "stdin", "stdout", or "stderr".

job.pid(id)

  • id (integer): job ID.
  • Returns (integer|string|nil): the process ID (PID) of the running job, or nil if the job doesn't exist.

Example:

local job = require('job')
local jobid = job.start({ 'sleep', '10' }, {})
local pid = job.pid(jobid)
if pid then
    print('Job PID:', pid)
    -- You can use the PID with external tools, for example:
    -- vim.fn.system('kill -9 ' .. pid)  -- force kill the process
end

job.is_running(id)

  • id (integer): job ID.

job.wait(id, timeout)

  • id (integer): job ID.
  • timeout (integer): maximum waiting time in milliseconds.

🎯 Job options

All options are optional.

encoding

If the output encoding of a job command is not UTF‑8, set this option to convert the output automatically.

Example:

job.start({ 'iconv', '-f', 'gbk', 'file.txt' }, {
    encoding = 'gbk',
    on_stdout = function(id, data) vim.print(data) end,
})

cwd

Working directory for the job.

job.start({ 'pwd' }, { cwd = '/tmp' })

detached

If true, the job will run in a detached state (see uv.spawn documentation).

clear_env

If true, only the environment variables provided in env will be passed to the job. By default the job inherits the current environment (with some Neovim‑specific variables removed).

env

Table of environment variables to set for the job. Values can be strings or numbers.

job.start({ 'printenv', 'MY_VAR' }, {
    env = { MY_VAR = 'hello' },
    on_stdout = function(id, data) vim.print(data) end,
})

timeout

Timeout in milliseconds after which the job will be automatically terminated with SIGTERM (signal 15). Useful for preventing long-running jobs from hanging indefinitely.

-- Kill the job if it runs longer than 5 seconds
job.start({ 'sleep', '10' }, {
    timeout = 5000,
    on_exit = function(id, code, signal)
        -- code: 143 (128 + 15) means killed by SIGTERM
        print('job ' .. id .. ' exited with code ' .. code)
    end,
})

❌ Error codes

code meaning
0 invalid command type, empty command string, or empty command table
-1 command is not executable, or uv.spawn failed
-2 opts.cwd exists but is not a directory

πŸ“¨ Callback signatures

on_stdout(id, data[, stream])

Called when the job writes to stdout.

  • id (integer): job ID.
  • data (table): list of output lines (strings).
  • stream (string, optional): if the callback accepts three parameters, this will be "stdout".

on_stderr(id, data[, stream])

Called when the job writes to stderr. Same signature as on_stdout.

on_exit(id, code, signal)

Called when the job exits.

  • id (integer): job ID.
  • code (integer): exit code.
  • signal (integer): signal number (0 if the job exited normally).

πŸ“£ Self-Promotion

Like this plugin? Star the repository on GitHub.

Love this plugin? Follow me on GitHub or Twitter.

πŸ’¬ Feedback

If you encounter any bugs or have suggestions, please file an issue in the issue tracker

πŸ™ Credits

  • vim.uv β€” libuv bindings for Neovim
  • plenary.nvim β€” inspiration for the job API design

πŸ“„ License

Licensed under GPL-3.0.

About

async job control api for neovim

Topics

Resources

Stars

15 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages