What's the general "anatomy" of an RPC-based plugin?

MacOS has the ability to change the system color scheme based on time of day (between “light mode” and “dark mode”), and the user can manually change it as well.

It is easy to query the color scheme from the command line:

defaults read -g AppleInterfaceStyle

How would you write a plugin to automatically change the color scheme when the output of this command changes?

I was thinking that you would write a script that watches for a change (maybe polling every few seconds?), which then calls the :colorscheme command via the RPC API (with nvim_command()). Or maybe use the technique described here for monitoring changes to the relevant settings file on the machine.

On the Neovim side, what’s the basic anatomy of an RPC-based plugin? Are there any good examples of such plugins that I should look at? I assume you call jobstart() somewhere in a Vimscript or Lua plugin, but beyond that I’m not sure what the best practices are (as well as any caveats to be aware of).

Since this is a fairly simple plugin (just reading the output of a process and doing some commands and such based on its output), to create a plugin doing this, I’d almost certainly use Lua instead of an RPC plugin:

Lua color-changing script
local Job = require('plenary.job')

local timer = vim.loop.new_timer()
timer:start(1000, 1000, vim.schedule_wrap(function()
  local output
  local job = Job:new {
    command = 'defaults',
    args = {'read', '-g', 'AppleInterfaceStyle'},

    on_stdout = function(_, data)
      output = data
    end
  }

  job:sync()

  if output == 'Dark' then
    vim.cmd [[ colorscheme gruvbox-material ]]
  else
    vim.cmd [[ colorscheme elflord ]]
  end
end))

The above should be fairly self-explanatory, but just in case it isn’t, here are a few things to note. I’ve used vim.loop to create a timer, which you can find the docs for here: luv/docs.md at master · luvit/luv · GitHub (see also :help vim.loop). In addition, the above uses plenary.nvim for the nice job API, which afaik is just a wrapper over vim.loop's process capabilities.

Running the above via :luafile % will change your colorscheme to gruvbox-material or elflord (assuming you have those installed) depending on whether your system theme is light or dark (I tested it and seemed to work fine).

Note that you could add more to this, perhaps wrapping it in a function that allows you to pass the colorschemes, and that returns the timer so you can stop it; or maybe you could read from global variables to get the colorschemes, so you can change them on the fly; etc.

However, just because I’d personally do this in Lua because of how much easier it is to setup, it definitely can be done in an RPC plugin. I’ve written a quick Rust example to do almost the exact same thing as the above Lua script, just with the extra step of having to run :lua require'colorscheme_changer'.run() to start it: GitHub - smolck/nvim-rpc-plugin-example: Quick colorscheme changing example

You asked some questions regarding RPC plugins/how they work, and hopefully poking around in that repo (specifically the lua/ directory) will give you the information you want. I also have a spotify plugin I created/am working on that is another, perhaps more instructive, example which enables you to play Spotify tracks via Telescope and also gives a couple other commands. It’s a WIP, will hopefully improve soon, but shows the basic idea of how to setup an RPC plugin.

See also GitHub - tjdevries/rofl.nvim: Rust On the FLY completion for neovim, which is where I got most of the Lua code to start a Rust plugin over RPC, and the examples in nvim-rs’s repo, if you’re interested in the Rust side of things.

Note that one of the downsides of using, say, a Rust plugin is the compilation step. The user has to go to the installed plugin’s directory and run cargo build --release in the case of all of the plugins I linked, which while it works, isn’t ideal (this also means they have to have the Rust toolchain installed, another downside).

As for best practices, I’m not sure there really are any. It’s kind of up to you to organize things as you like. Perhaps there are some though that I’m not aware of/not thinking of.

3 Likes

Wow, thanks for the detailed reply! I’ll go over these examples and try to extract some general concepts from them. I’m specifically curious about how (and when) to start and manage the remote RPC client from within Neovim.

As for why not Lua, I kind of like the filesystem watcher technique (instead of polling), and it’s an excuse to have fun writing some code in a language I don’t normally use.

You can see this in the Lua parts of the examples I linked, but the way rofl.nvim does it that I’ve started using, is you basically have a start function that calls vim.fn.jobstart({ plugin_binary_path }, { rpc = true }), and stores the job_id returned from that call inside a Lua module (the main one for your plugin, most likely) for later use. Then, whenever you do a request to the plugin (through vim.notify or whatever), you first check if the plugin has been started (by seeing if the job_id is set), and if not, you start it before sending it a request or notify (and set the job_id accordingly).

I’m not sure if this is how it’s done in other plugins, but I think this method is good enough for what I want to do. If someone has a way they think is better though I’d be interested in hearing it.

Another upside to using an RPC plugin instead of a Lua plugin that I didn’t mention, is that you can use libraries for the language you’re using that Lua may not have. For example, that’s why I’m using Rust for that spotify plugin: I can use the rspotify crate which makes everything a lot easier than it otherwise would be in Lua, which I don’t think has such a library.