smart-splits.nvim

repository·master·Indexed 23 days ago

https://github.com/mrjones2014/smart-splits.nvim

A Neovim plugin for intuitive split pane management using a directional mental model for resizing and navigation. It provides seamless integration with terminal multiplexers including Tmux, Zellij, Wezterm, and Kitty, allowing users to move and resize panes across both Neovim and the multiplexer environment.

Tokens
5.6K
Snippets
8
Records
10
Agent score
33%

What's inside smart-splits.nvim

  1. How custom edge behavior functions work

    master

    If you set at_edge to a function, the function will be called with a context object. This allows you to implement custom logic for when a user attempts to move past the edge of a split.

    The context object contains:

    • mux: An object for interacting with the multiplexer (Tmux, Wezterm, Kitty, or Zellij).
      • type: 'tmux'|'wezterm'|'kitty'|'zellij'
      • current_pane_id(): Returns the current pane ID.
      • is_in_session(): Boolean.
      • current_pane_is_zoomed(): Boolean.
      • current_pane_at_edge(direction): Returns true if the pane is at the specified edge.
      • next_pane(direction): Returns true if successful.
      • resize_pane(direction): Returns true if successful.
      • split_pane(direction, size): Returns true if successful.
    • direction: 'left'|'right'|'up'|'down'
    • split(): Utility to split the current Neovim pane in the current direction.
    • wrap(): Utility to wrap to the opposite Neovim pane.
  2. Install smart-splits.nvim

    master

    You can install smart-splits.nvim using either Packer.nvim or Lazy.nvim. The plugin supports semantic versioning via git tags. If you want to use Kitty terminal multiplexer support, you must run the provided post-install hook.

    Using Packer.nvim

    use('mrjones2014/smart-splits.nvim')
    -- or use a specific version
    use({ 'mrjones2014/smart-splits.nvim', tag = 'v1.0.0' })
    -- to use Kitty multiplexer support, run the post install hook
    use({ 'mrjones2014/smart-splits.nvim', run = './kitty/install-kittens.bash' })

    Using Lazy.nvim

    { 'mrjones2014/smart-splits.nvim' }
    -- or use a specific version, or a range of versions using lazy.nvim's version API
    { 'mrjones2014/smart-splits.nvim', version = '>=1.0.0' }
    -- to use Kitty multiplexer support, run the post install hook
    { 'mrjones2014/smart-splits.nvim', build = './kitty/install-kittens.bash' }
    use('mrjones2014/smart-splits.nvim')
    -- or use a specific version
    use({ 'mrjones2014/smart-splits.nvim', tag = 'v1.0.0' })
    -- to use Kitty multiplexer support, run the post install hook
    use({ 'mrjones2014/smart-splits.nvim', run = './kitty/install-kittens.bash' })
  3. Integrate with Tmux

    master

    Enables seamless navigation between Neovim splits and Tmux panes.

    Important: Do not lazy-load smart-splits.nvim when using this integration, as it relies on the @pane-is-vim tmux variable which is only set when the plugin loads.

    Using TPM

    Add the plugin to your TPM configuration. You can customize keys and resize steps using @smart-splits_* variables.

    Manual Configuration

    Add bindings to your ~/.tmux.conf to check for the @pane-is-vim variable. If present, send keys to Neovim; otherwise, use standard Tmux pane selection/resizing.

    # Using TPM
    set -g @plugin 'mrjones2014/smart-splits.nvim'
    set -g @smart-splits_no_wrap ''
    set -g @smart-splits_move_left_key  'C-h'
    set -g @smart-splits_move_down_key  'C-j'
    set -g @smart-splits_move_up_key    'C-k'
    set -g @smart-splits_move_right_key 'C-l'
    set -g @smart-splits_resize_left_key  'M-h'
    set -g @smart-splits_resize_down_key  'M-j'
    set -g @smart-splits_resize_up_key    'M-k'
    set -g @smart-splits_resize_right_key 'M-l'
    set -g @smart-splits_resize_step_size '3'
    
    # Manual bindings for ~/.tmux.conf
    bind-key -n C-h if -F "#{@pane-is-vim}" 'send-keys C-h'  'select-pane -L'
    bind-key -n C-j if -F "#{@pane-is-vim}" 'send-keys C-j'  'select-pane -D'
    bind-key -n C-k if -F "#{@pane-is-vim}" 'send-keys C-k'  'select-pane -U'
    bind-key -n C-l if -F "#{@pane-is-vim}" 'send-keys C-l'  'select-pane -R'
    
    bind-key -n M-h if -F "#{@pane-is-vim}" 'send-keys M-h' 'resize-pane -L 3'
    bind-key -n M-j if -F "#{@pane-is-vim}" 'send-keys M-j' 'resize-pane -D 3'
    bind-key -n M-k if -F "#{@pane-is-vim}" 'send-keys M-k' 'resize-pane -U 3'
    bind-key -n M-l if -F "#{@pane-is-vim}" 'send-keys M-l' 'resize-pane -R 3'
  4. Integrate with Kitty

    master

    The plugin sets a Kitty user-variable IS_NVIM on load. Use Kitty's conditional mappings to route keys to Neovim when IS_NVIM is true, and to Kitty's own window navigation when false.

    Important: You must allow Kitty to listen for remote commands.

    Setup Steps

    1. Configure allow_remote_control and listen_on in kitty.conf.
    2. Add conditional mappings in kitty.conf for navigation and resizing.
    3. If using Neovim over SSH, you must forward the Kitty socket using kitten ssh and ensure your local ssh.conf has forward_remote_control yes.

    Note: config.at_edge = 'wrap' is not supported in Kitty because the plugin cannot determine pane layout via CLI.

    # kitty.conf
    map ctrl+j neighboring_window down
    map ctrl+k neighboring_window up
    map ctrl+h neighboring_window left
    map ctrl+l neighboring_window right
    
    # Unset the mapping to pass the keys to neovim
    map --when-focus-on var:IS_NVIM ctrl+j
    map --when-focus-on var:IS_NVIM ctrl+k
    map --when-focus-on var:IS_NVIM ctrl+h
    map --when-focus-on var:IS_NVIM ctrl+l
    
    # Resizing
    map alt+j kitten relative_resize.py down  3
    map alt+k kitten relative_resize.py up    3
    map alt+h kitten relative_resize.py left  3
    map alt+l kitten relative_resize.py right 3
    
    map --when-focus-on var:IS_NVIM alt+j
    map --when-focus-on var:IS_NVIM alt+k
    map --when-focus-on var:IS_NVIM alt+h
    map --when-focus-on var:IS_NVIM alt+l
    
    allow_remote_control yes
    listen_on unix:/tmp/mykitty
  5. Integrate with Zellij

    master

    Zellij support is implemented via the vim-zellij-navigator plugin.

    Note: Resizing splits by a specific amount from Neovim and presetting the new size is currently unsupported in Zellij.

    To use this, add keybindings to your Zellij KDL configuration that call the vim-zellij-navigator.wasm plugin with move_focus or resize payloads. It is highly recommended to use the local path to the .wasm file rather than the GitHub URL.

    keybinds {
      shared_except "locked" {
        bind "Ctrl h" {
            MessagePlugin "https://github.com/hiasr/vim-zellij-navigator/releases/download/0.2.1/vim-zellij-navigator.wasm" {
                name "move_focus";
                payload "left";
            };
        }
        // ... repeat for j (down), k (up), l (right) and Alt keys for resize
      }
    }
  6. Integrate with Wezterm

    master

    Requires the wezterm CLI to be on your $PATH.

    Note: Pane resizing currently requires a nightly build of Wezterm.

    If you are on Wezterm nightly, use wezterm.plugin.require to load the plugin and smart_splits.apply_to_config(config, opts) to apply it to your configuration.

    Manual Configuration (Standard Builds)

    If not using the plugin loader, you must define an is_vim(pane) function in your wezterm.lua to detect Neovim and use w.action_callback to route keys between Wezterm and Neovim.

    -- Experimental Plugin Loader (Nightly)
    local wezterm = require('wezterm')
    local smart_splits = wezterm.plugin.require('https://github.com/mrjones2014/smart-splits.nvim')
    local config = wezterm.config_builder()
    
    smart_splits.apply_to_config(config, {
      direction_keys = { 'h', 'j', 'k', 'l' },
      modifiers = {
        move = 'CTRL',
        resize = 'META',
      },
    })
  7. Configure smart-splits.nvim

    master

    Configure the plugin using require('smart-splits').setup().

    Important Note on Lazy-loading: Do not lazy-load smart-splits.nvim. The plugin sets a pane-local option '@pane-is-vim' on load, which is unset when Neovim exits or suspends. If the plugin is lazy-loaded, this variable won't be set when needed.

    Key Configuration Concepts:

    • Ignoring Buffers/Filetypes: Use ignored_buftypes and ignored_filetypes to prevent certain buffers (like sidebars or quickfix windows) from affecting resizing logic. This only affects resizing, not movement.
    • Edge Behavior (at_edge): Defines what happens when your cursor is at the edge of a split and you move further in that direction. Options include 'wrap' (wrap to opposite side), 'split' (create new split), 'stop' (do nothing), or a custom function.
    • Multiplexer Integration: Automatically detected via environment variables, but can be explicitly set via multiplexer_integration or the global variable vim.g.smart_splits_multiplexer_integration before loading.
    • Floating Windows: Control behavior when a window is floating using float_win_behavior ('previous' to focus previous window, or 'mux' to forward to the multiplexer).
    require('smart-splits').setup({
      -- Ignored buffer types (only while resizing)
      ignored_buftypes = {
        'nofile',
        'quickfix',
        'prompt',
      },
      -- Ignored filetypes (only while resizing)
      ignored_filetypes = { 'NvimTree' },
      -- the default number of lines/columns to resize by at a time
      default_amount = 3,
      -- Desired behavior when your cursor is at an edge and you
      -- are moving towards that same edge:
      -- 'wrap' => Wrap to opposite side
      -- 'split' => Create a new split in the desired direction
      -- 'stop' => Do nothing
      -- function => You handle the behavior yourself
      at_edge = 'wrap',
      -- Desired behavior when the current window is floating:
      -- 'previous' => Focus previous Vim window and perform action
      -- 'mux' => Always forward action to multiplexer
      float_win_behavior = 'previous',
      -- when moving cursor between splits left or right,
      -- place the cursor on the same row of the *screen*
      -- regardless of line numbers. False by default.
      move_cursor_same_row = false,
      -- whether the cursor should follow the buffer when swapping
      -- buffers by default; it can also be controlled by passing
      -- `{ move_cursor = true }` or `{ move_cursor = false }`
      -- when calling the Lua function.
      cursor_follows_swapped_bufs = false,
      -- ignore these autocmd events (via :h eventignore) while processing
      -- smart-splits.nvim computations, which involve visiting different
      -- buffers and windows. These events will be ignored during processing,
      -- and un-ignored on completed. This only applies to resize events,
      -- not cursor movement events.
      ignored_events = {
        'BufEnter',
        'WinEnter',
      },
      -- enable or disable a multiplexer integration;
      -- automatically determined, unless explicitly disabled or set,
      -- by checking the $TERM_PROGRAM environment variable,
      -- and the $KITTY_LISTEN_ON environment variable for Kitty.
      -- You can also set this value by setting `vim.g.smart_splits_multiplexer_integration`
      -- before the plugin is loaded (e.g. for lazy environments).
      multiplexer_integration = nil,
      -- disable multiplexer navigation if current multiplexer pane is zoomed
      -- NOTE: This does not work on Zellij as there is no way to determine the
      -- pane zoom state outside of the Zellij Plugin API, which does not apply here
      disable_multiplexer_nav_when_zoomed = true,
      -- Supply a Kitty remote control password if needed, or you can also set vim.g.smart_splits_kitty_password
      -- see https://sw.kovidgoyal.net/kitty.conf/#opt-kitty.remote_control_password
      kitty_password = nil,
      -- In Zellij, set this to true if you would like to move to the next *tab*
      -- when the current pane is at the edge of the zellij tab/window
      zellij_move_focus_or_tab = false,
      -- default logging level, one of: 'trace'|'debug'|'info'|'warn'|'error'|'fatal'
      log_level = 'info',
    })
  8. Recommended Key Mappings for smart-splits.nvim

    master

    The following Lua mappings are recommended for resizing splits, moving the cursor between splits, and swapping buffers.

    Note on Alt/Meta keys: In terminals like Alacritty or Ghostty on macOS, you may need to configure the terminal to treat the macOS Option key as Alt/Meta for these mappings to work.

    -- recommended mappings
    -- resizing splits
    -- these keymaps will also accept a range,
    -- for example `10<A-h>` will `resize_left` by `(10 * config.default_amount)`
    vim.keymap.set('n', '<A-h>', require('smart-splits').resize_left)
    vim.keymap.set('n', '<A-j>', require('smart-splits').resize_down)
    vim.keymap.set('n', '<A-k>', require('smart-splits').resize_up)
    vim.keymap.set('n', '<A-l>', require('smart-splits').resize_right)
    -- moving between splits
    vim.keymap.set('n', '<C-h>', require('smart-splits').move_cursor_left)
    vim.keymap.set('n', '<C-j>', require('smart-splits').move_cursor_down)
    vim.keymap.set('n', '<C-k>', require('smart-splits').move_cursor_up)
    vim.keymap.set('n', '<C-l>', require('smart-splits').move_cursor_right)
    vim.keymap.set('n', '<C-\
    angle', require('smart-splits').move_cursor_previous)
    -- swapping buffers between windows
    vim.keymap.set('n', '<leader><leader>h', require('smart-splits').swap_buf_left)
    vim.keymap.set('n', '<leader><leader>j', require('smart-splits').swap_buf_down)
    vim.keymap.set('n', '<leader><leader>k', require('smart-splits').swap_buf_up)
    vim.keymap.set('n', '<leader><leader>l', require('smart-splits').swap_buf_right)
  9. Access the Multiplexer Lua API

    master

    For scripting purposes, you can access the currently active multiplexer backend using require('smart-splits.mux').get(). This returns a SmartSplitsMultiplexer object or nil if no integration is active.

    SmartSplitsMultiplexer Interface

    • current_pane_id(): Returns the current pane ID.
    • current_pane_at_edge(direction: 'left'|'right'|'up'|'down'): Returns boolean.
    • is_in_session(): Returns boolean.
    • current_pane_is_zoomed(): Returns boolean.
    • next_pane(direction: 'left'|'right'|'up'|'down'): Returns boolean.
    • resize_pane(direction: 'left'|'right'|'up'|'down', amount: number): Returns boolean.
    • split_pane(direction: 'left'|'right'|'up'|'down', size: number|nil): Returns boolean.
    • type: Returns 'tmux' | 'wezterm' | 'kitty' | 'zellij'.
  10. Lua API Reference

    master

    The smart-splits module provides functions for split manipulation.

    Resizing Splits

    Functions accept an amount (defaults to 3). Use absolute values (no + or -). These functions also support range prefixes (e.g., 10<A-h> resizes by 10 * config.default_amount).

    • resize_up(amount)
    • resize_down(amount)
    • resize_left(amount)
    • resize_right(amount)

    Moving Between Splits

    • move_cursor_up({ same_row = boolean, at_edge = 'wrap' | 'split' | 'stop' })
    • move_cursor_down()
    • move_cursor_left()
    • move_cursor_right()
    • move_cursor_previous()

    Swapping Buffers

    Swaps the buffer in the specified direction with the window in that direction. You can pass an opts table to control if the cursor follows the buffer.

    • swap_buf_up()
    • swap_buf_down()
    • swap_buf_left()
    • swap_buf_right({ move_cursor = boolean })
    -- resizing splits
    require('smart-splits').resize_up(amount)
    require('smart-splits').resize_down(amount)
    require('smart-splits').resize_left(amount)
    require('smart-splits').resize_right(amount)
    
    -- moving between splits
    require('smart-splits').move_cursor_up({ same_row = boolean, at_edge = 'wrap' | 'split' | 'stop' })
    require('smart-splits').move_cursor_down()
    require('smart-splits').move_cursor_left()
    require('smart-splits').move_cursor_right()
    require('smart-splits').move_cursor_previous()
    
    -- Swapping buffers directionally
    require('smart-splits').swap_buf_up()
    require('smart-splits').swap_buf_down()
    require('smart-splits').swap_buf_left()
    require('smart-splits').swap_buf_right({ move_cursor = true })