1   Introduction

Neovim/Nvim supports a client-server style of control. You can send most operations to Nvim, including normal mode operations and command mode operations.

In addition, there is a Python application that can be run from the command line to send operations/commands to and Nvim instance. For example, you can start an instance of Nvim in one bash session, then, from another bash session, you can send that server commands to open files, perform editing operations on those files, save the file, and, in general, most anything you can do with your fingers from within Nvim.

Nvr (neovim-remote) is a Python script that can be run from the command line and that does most of this work for you. It has a variety of command line options that enable you to send commands to an instance of the Nvim text editor (the server).

For more information, see https://github.com/mhinz/neovim-remote.

You can install nvr with the following:

$ pip install neovim-remote

For help with nvr, type the following at the command line:

$ nvr --help

And, although Nvr does not appear to support a Python API, you can control Nvr programmatically. Below, I describe three approaches to doing that.

Alternatively, you can create your own API for Nvr. I give an example of that below.

2   Approaches

2.1   Run nvr from the command line

This appears to be the way that nvr is intended to be used. Here is an example of this use:

# load a file.
$ nvr --remote-send ':tabedit sample_file.txt<cr>'
# move down several lines.
$ nvr --remote-send 'jjjjjj'
# insert some text.
$ nvr --remote-send 'Isome sample text<esc>'
# undo the change
$ nvr --remote-send 'u'
# delete the Nvim buffer.
$ nvr --remote-send ':bd<cr>'

And, of course, if you want to automate this approach, you can put commands that are similar to the above in a bash script on Linux, or the equivalent DOS commands on MS Windows.

2.2   Call nvr.main

Another strategy is to import nvr in Python and then call the main function in nvr.py. When/if you do that, you will need to spoof Nvr into thinking that it is being run from the command line by passing it the same Python list of command line arguments that it would get (in sys.argv). That works, and it is not hard to do, but it's not as slick as if Nvr supported an API for the various commands and options that it supports.

Here is a Python script that follows this approach. It implements three commands: (1) create (load files), (2) insert (insert text in files, and (3) remove (remove the files/buffers from Nvim.

#!/usr/bin/env python

"""
synopsis:
  A Python client for remote control of Nvim.

options:
  -h, --help            show this help message and exit
  -s SERVERNAME, --servername SERVERNAME
                        the address/name of the Nvim server
  -o [OPERATIONS ...], --operations [OPERATIONS ...]
                        operations. a list of: create | insert | remove
  -v, --verbose         Print messages during actions.

examples:
  python nvim_client.py create insert
  python nvim_client.py remove
notes:
  This Nvim client uses a default server address.
  So, you need to start Nvim with `nvim --listen /tmp/nvimsocket`, which is
    the default server address used by Nvr clients.
"""


# import sys
import argparse
import nvr

Progname = './nvim_client.py'
Commands_create = [
    ':tabedit sample_file.txt<CR>',
    ':tabedit another_file.txt<CR>',
]
Commands_insert = [
    'gT',
    'jjjjIhello dave<esc>',
    'gt',
    '8jIgoodbye dave<esc>',
]
Commands_remove = [
    ':bd!<cr>',
    ':bd!<cr>',
]

def dbg_msg(options, msg):
    """Print a message if verbose is on."""
    if options.verbose:
        print(msg)

def run(opts):
    for operation in opts.operations:
        if operation.lower() == 'create':
            for command in Commands_create:
                arguments = [Progname,
                             '--remote-send', command]
                dbg_msg(
                    opts,
                    '(nvim_client.py) arguments: {}'.format(arguments))
                nvr.main(arguments)
        elif operation.lower() == 'insert':
            for command in Commands_insert:
                arguments = [Progname,
                             '--remote-send', command]
                dbg_msg(
                    opts,
                    '(nvim_client.py) arguments: {}'.format(arguments))
                nvr.main(arguments)
        elif operation.lower() == 'remove':
            for command in Commands_remove:
                arguments = [Progname,
                             '--remote-send', command]
                dbg_msg(
                    opts,
                    '(nvim_client.py) arguments: {}'.format(arguments))
                nvr.main(arguments)
        else:
            print('bad operation: {}'.format(operation))

def main():
    description = """\
synopsis:
  A Python client for remote control of Nvim.
"""
    epilog = """\
examples:
  python nvim_client.py create insert
  python nvim_client.py remove
notes:
  This Nvim client uses a default server address.
  So, you need to start Nvim with `nvim --listen /tmp/nvimsocket`, which is
    the default server address used by Nvr clients.
"""
    parser = argparse.ArgumentParser(
        description=description,
        epilog=epilog,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "-s", "--servername",
        type=str,
        required=False,
        help="the address/name of the Nvim server",
    )
    parser.add_argument(
        "-o", "--operations",
        type=str, default='create',
        nargs='*',
        required=False,
        help="operations.  a list of: create | insert | remove",
    )
    parser.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Print messages during actions.",
    )
    options = parser.parse_args()
    run(options)

if __name__ == '__main__':
    main()

2.3   Run instances of the Nvr executable script in a subprocess

If you are a Pythonista, you can use the Python subprocess library and to run external instances of Nvr as a subprocess.

There is no reason to restrict this approach to Python. I suppose it could be done in any language that supports subprocesses.

Here is a simple Python script that illustrates this approach:

import subprocess

def create():
    subprocess.run(['nvr', '--remote-send', ':tabedit sample_file.txt<cr>'])
    subprocess.run(['nvr', '--remote-send', 'jjjIabracadabra<esc>'])

def remove():
    subprocess.run(['nvr', '--remote-send', 'u'])
    subprocess.run(['nvr', '--remote-send', ':bd<cr>'])

Notes:

  • The first function (create) loads a file, moves the cursor down three lines, and then insert a few characters.
  • The second function (remove), undoes the change to the file and deletes the Nvim buffer.
  • For more on the Python subprocess module see -- https://docs.python.org/3/library/subprocess.html
  • The Python subprocess module offers additional ways and options for running executables in a subprocess.

3   Creating your own API for Nvr

Depending on the complexity of your needs, it can be quite easy to create a Python module that provides an API that mimics some of the behavior and capabilities of the command line version of Nvr. It means implementing a Python module that exposes functions (or classes and methods if you pursue an object oriented approach) each of which replicates some part of the functionality in nvr.py itself.

If you decide to pursue this "do it yourself" approach, a quick way to get started is to look at, copy, and modify code that is in function proceed_after_attach in nvr.py inside your installation of neovim-remote. And, if you examine the sample API included below, you will see that this is what I have done: I've copied snippets of Nvr code and wrapped a bit of interface code around each of them. My thanks to the implementers of neovim-remote for that code.

So, it's reasonably easy to create your own API that exposes select capabilities of Nvr. I've implemented a simple example that exposes Nvr's functionality for --remote-send and --remote-expr. Here it is:

#!/usr/bin/env python

import sys
import os
import textwrap
from nvr import nvr as nvr_remote

def attach(address='/tmp/nvimsocket'):
    nvr = nvr_remote.Nvr(address)
    nvr.attach()
    return nvr.server

def remote_send(server, commands):
    server.input(commands)

def remote_expr(server, expr):
    result = ''
    #if options.remote_expr == '-':
    #    options.remote_expr = sys.stdin.read()
    try:
        result = server.eval(expr)
    except:
        print(textwrap.dedent(f"""
            No valid expression: {expr}
            Test it in Neovim: :echo eval('...')
            If you want to execute a command, use -c or -cc instead.
        """))
    if type(result) is bytes:
        result = result.decode()
    elif type(result) is list:
        result = str(list(map(lambda x: x.decode() if type(x) is bytes else x, result)))
    elif type(result) is dict:
        result = str({ (k.decode() if type(k) is bytes else k): v for (k,v) in result.items() })
    else:
        result = str(result)
    return result

def test():
    args = sys.argv[1:]
    if len(args) != 1:
        sys.exit("usage: python nvrapi.py <file-to-load>")
    filename = args[0]
    server = attach()
    remote_send(server, ':tabedit {}<cr>'.format(filename))
    remote_send(server, 'jj')
    remote_send(server, 'Iabracadabra<esc>')
    remote_send(server, 'jjjjIanother string<esc>')
    result = remote_expr(server, 'bufname("")')
    print('result: "{}"'.format(result))

if __name__ == '__main__':
    test()

Notes:

  • In order to actually, use and/or run the above script, you will need to: (1) install neovim-remote and (2) start an instance of the Nvim text editor with this command line:

    $ nvim --listen /tmp/nvimsocket
    

    See https://github.com/mhinz/neovim-remote for more help and information.

  • Function attach creates the connection and returns a "server". The default server address is /tmp/nvimsocket.

  • Function remote_send mimics the functionality of $ nvr --remote-send. It sends a sequence of commands (a command string) to Nvim.

  • Function remote_expr mimics the functionality of $ nvr --remote-expr. It sends a command to Nvim and returns the result that is returned by Nvim.

  • And function test provides a simple "proof of concept" and test that exercises and uses the functions in the API. It (1) loads a file into the remote instance of Nvim; (2) inserts several text strings into that file; and (3) asks the remote Nvim instance for the name of the file.


Published

Category

nvim

Tags

Contact