Neovim input methods, remote clipboard and pane navigation

On this page

Input methods, the terminal, the multiplexer and Neovim all participate in remote and multilingual editing. When an action stops working after adding SSH or Zellij, inspect that chain one layer at a time.

Normal mode and input methods

Normal-mode j is a motion command. An active composition input method may consume it before Neovim receives it. Switching to a Latin keyboard layout on leaving insert mode avoids that conflict.

For a Linux desktop using Fcitx5, query the current method:

fcitx5-remote -n

After adding keyboard-us to the available methods, test switching:

fcitx5-remote -s keyboard-us

Only after this works, save the plugin configuration as lua/plugins/input.lua:

return {
  {
    "keaising/im-select.nvim",
    event = "VeryLazy",
    opts = {
      default_im_select = "keyboard-us",
      default_command = "fcitx5-remote",
      set_previous_events = { "InsertEnter" },
    },
  },
}

Check both leaving insert mode and restoring the previous method on entering it. macOS, Windows and IBus need different commands. im-select.nvim

A remote Neovim normally cannot control the local desktop's Fcitx5. Installing the command remotely does not forward that operation through SSH; use an appropriate local terminal/input strategy instead.

Registers and the system clipboard

Internal registers do not require a desktop. "+y requests a clipboard provider. Wayland commonly uses wl-clipboard; OSC52 can send copied text back through an SSH terminal connection.

Inspect :checkhealth vim.provider, then test ordinary y separately from "+y. Ordinary copying uses the clipboard automatically only with settings such as clipboard=unnamedplus.

OSC52 writing and reading are separate capabilities. Terminal and multiplexer support, permissions and detection all matter. Clipboard provider

Send OSC52 and retain a local paste cache

Put this in lua/config/options.lua on a Neovim version providing vim.ui.clipboard.osc52. It sends copies outward but reads only the most recently copied contents in this Neovim process:

local osc52 = require("vim.ui.clipboard.osc52")
local clipboard_cache = { { "" }, "v" }

local function copy_to(register)
  local send = osc52.copy(register)
  return function(lines, regtype)
    clipboard_cache = { vim.deepcopy(lines), regtype }
    send(lines, regtype)
  end
end

local function paste_cached()
  return vim.deepcopy(clipboard_cache)
end

vim.g.clipboard = {
  name = "OSC52 with local cache",
  copy = { ["+"] = copy_to("+"), ["*"] = copy_to("*") },
  paste = { ["+"] = paste_cached, ["*"] = paste_cached },
}
vim.opt.clipboard = "unnamedplus"

This deliberately does not read newly copied browser content into the cache. Use the local terminal's paste action in insert mode for external text, or configure and verify a provider that supports actual clipboard reads.

Test local without multiplexer, local with Zellij, SSH without multiplexer and SSH inside Zellij. Copy a different marker each time and inspect the local desktop clipboard to locate the failing layer.

Configure the Neovim side

Save as lua/plugins/navigation.lua:

return {
  {
    "mrjones2014/smart-splits.nvim",
    opts = { at_edge = "stop", zellij_move_focus_or_tab = false },
    keys = {
      { "<C-h>", function() require("smart-splits").move_cursor_left() end, desc = "Move left" },
      { "<C-j>", function() require("smart-splits").move_cursor_down() end, desc = "Move down" },
      { "<C-k>", function() require("smart-splits").move_cursor_up() end, desc = "Move up" },
      { "<C-l>", function() require("smart-splits").move_cursor_right() end, desc = "Move right" },
    },
  },
}

This stops at the outer boundary and does not cross Zellij tabs. First verify movement between Neovim windows, then across the editor boundary. smart-splits

Configure the Zellij side

Download a compatible WASM release from vim-zellij-navigator. Add these bindings to the existing locked mode inside keybinds, replacing all four /absolute/path/ values:

locked {
    bind "Ctrl h" {
        MessagePlugin "file:/absolute/path/vim-zellij-navigator.wasm" {
            name "move_focus"; payload "left"; move_mod "ctrl";
        }
    }
    bind "Ctrl j" {
        MessagePlugin "file:/absolute/path/vim-zellij-navigator.wasm" {
            name "move_focus"; payload "down"; move_mod "ctrl";
        }
    }
    bind "Ctrl k" {
        MessagePlugin "file:/absolute/path/vim-zellij-navigator.wasm" {
            name "move_focus"; payload "up"; move_mod "ctrl";
        }
    }
    bind "Ctrl l" {
        MessagePlugin "file:/absolute/path/vim-zellij-navigator.wasm" {
            name "move_focus"; payload "right"; move_mod "ctrl";
        }
    }
}

This is an inner fragment, not a complete configuration. Merge it with the existing Ctrl+g mode switch. Review the plugin source and purpose when granting requested permissions. If using another default mode, place mappings where they actually apply.

Create two Neovim windows in a left pane and a shell on the right. Verify movement within Neovim, into the shell, back into Neovim and stopping at the outer edge. On failure inspect :verbose nmap <C-l>, then the Zellij mode, plugin loading and terminal mappings.

Older internal-function overrides are version-specific workarounds. Start with public options and add an adaptation only for a reproduced issue, recording when it should be removed.