Skip to content

Repository files navigation

title no-neck-pain.nvim
description Dead simple plugin to center the currently focused buffer to the middle of the screen
category neovim-plugin
plugin_type ui-enhancement
keywords
neovim
buffer
centering
zen-mode
focus
distraction-free
version >=3.0.0
requires neovim >= 0.10
repository shortcuts/no-neck-pain.nvim

☕ no-neck-pain.nvim

Dead simple plugin to center the currently focused buffer to the middle of the screen.

showcase.mp4

⚡️ Features

Creates evenly sized empty buffers on each side of your focused buffer, which acts as padding for your window.

Want to see it in action? Take a look at the showcase section

📋 Installation

Package manager Snippet

wbthomason/packer.nvim

-- stable version
use {"shortcuts/no-neck-pain.nvim", tag = "*" }
-- dev version
use {"shortcuts/no-neck-pain.nvim"}

junegunn/vim-plug

-- stable version
Plug 'shortcuts/no-neck-pain.nvim', { 'tag': '*' }
-- dev version
Plug 'shortcuts/no-neck-pain.nvim'

folke/lazy.nvim

-- stable version
require("lazy").setup({{"shortcuts/no-neck-pain.nvim", version = "*"}})
-- dev version
require("lazy").setup({"shortcuts/no-neck-pain.nvim"})

nix-community/nixvim

plugins.no-neck-pain.enable = true;

☄ Getting started

No configuration/setup steps needed! Sit back, relax and call :NoNeckPain.

⚙ Configuration

Note: Need some inspiration on customizing your experience? Take a look at the showcase

Click to unfold the full list of options with their default values

Note: The options are also available in Neovim by using:

  • :h NoNeckPain.options to see the global plugin options.
  • :h NoNeckPain.bufferOptions to see the side buffer options.
require("no-neck-pain").setup({
    -- Prints useful logs about triggered events, and reasons actions are executed.
    ---@type boolean
    debug = false,
    -- The width of the focused window that will be centered. When the terminal width is less than the `width` option, the side buffers won't be created.
    ---@type integer|"textwidth"|"colorcolumn"
    width = 100,
    -- Represents the lowest width value a side buffer should be.
    -- This option can be useful when switching window size frequently, example:
    -- in full screen screen, width is 210, you define an NNP `width` of 100, which creates each side buffer with a width of 50. If you resize your terminal to the half of the screen, each side buffer would be of width 5 and thereforce might not be useful and/or add "noise" to your workflow.
    ---@type integer
    minSideBufferWidth = 10,
    -- Disables the plugin if the last valid buffer in the list have been closed.
    ---@type boolean
    disableOnLastBuffer = false,
    -- When `true`, disabling the plugin closes every other windows except the initially focused one.
    ---@usage: this parameter will be renamed `killAllWindowsOnDisable` in a future release.
    ---@type boolean
    killAllBuffersOnDisable = false,
    -- When `true`, deleting the main no-neck-pain buffer with `:bd`, `:bdelete` does not disable the plugin, it fallbacks on the newly focused window and refreshes the state by re-creating side-windows if necessary.
    ---@type boolean
    fallbackOnBufferDelete = true,
    -- Adds autocmd (@see `:h autocmd`) which aims at automatically enabling the plugin.
    ---@type table
    autocmds = {
        -- When `true`, enables the plugin when you start Neovim.
        -- If the main window is  a side tree (e.g. NvimTree) or a dashboard, the command is delayed until it finds a valid window.
        -- The command is cleaned once it has successfuly ran once.
        -- When `safe`, debounces the plugin before enabling it.
        -- This is recommended if you:
        --  - use a dashboard plugin, or something that also triggers when Neovim is entered.
        --  - usually leverage commands such as `nvim +line file` which are executed after Neovim has been entered.
        ---@type boolean | "safe"
        enableOnVimEnter = false,
        -- When `true`, enables the plugin when you enter a new Tab.
        -- note: it does not trigger if you come back to an existing tab, to prevent unwanted interfer with user's decisions.
        ---@type boolean
        enableOnTabEnter = false,
        -- When `true`, reloads the plugin configuration after a colorscheme change.
        ---@type boolean
        reloadOnColorSchemeChange = true,
        -- When `true`, entering one of no-neck-pain side buffer will automatically skip it and go to the next available buffer. This setting is omitted when scratch pad is enabled.
        ---@type boolean
        skipEnteringNoNeckPainBuffer = true,
    },
    -- Creates mappings for you to easily interact with the exposed commands.
    ---@type table
    mappings = {
        -- When `true`, creates all the mappings that are not set to `false`.
        ---@type boolean
        enabled = false,
        -- Sets a global mapping to Neovim, which allows you to toggle the plugin.
        -- When `false`, the mapping is not created.
        ---@type string
        toggle = "<Leader>np",
        -- Sets a global mapping to Neovim, which allows you to toggle the left side buffer.
        -- When `false`, the mapping is not created.
        ---@type string
        toggleLeftSide = "<Leader>nql",
        -- Sets a global mapping to Neovim, which allows you to toggle the right side buffer.
        -- When `false`, the mapping is not created.
        ---@type string
        toggleRightSide = "<Leader>nqr",
        -- Sets a global mapping to Neovim, which allows you to increase the width (+5) of the main window.
        -- When `false`, the mapping is not created.
        ---@type string | { mapping: string, value: number }
        widthUp = "<Leader>n=",
        -- Sets a global mapping to Neovim, which allows you to decrease the width (-5) of the main window.
        -- When `false`, the mapping is not created.
        ---@type string | { mapping: string, value: number }
        widthDown = "<Leader>n-",
        -- Sets a global mapping to Neovim, which allows you to toggle the scratchPad feature.
        -- When `false`, the mapping is not created.
        ---@type string
        scratchPad = "<Leader>ns",
        -- Sets a global mapping to Neovim, which allows you to toggle the debug mode.
        -- When `false`, the mapping is not created.
        ---@type string
        debug = "<Leader>nd",
    },
    --- Common options that are set to both side buffers.
    --- See |NoNeckPain.bufferOptions| for option scoped to the `left` and/or `right` buffer.
    ---@type table
    buffers = {
        -- When `true`, the side buffers will be named `no-neck-pain-left` and `no-neck-pain-right` respectively.
        ---@type boolean
        setNames = false,
        -- Leverages the side buffers as notepads, which work like any Neovim buffer and automatically saves its content at the given `location`.
        -- note: quitting an unsaved scratchPad buffer is non-blocking, and the content is still saved.
        --- see |NoNeckPain.bufferOptionsScratchPad|
        scratchPad = {
            -- When `true`, automatically sets the following options to the side buffers:
            -- - `autowriteall`
            -- - `autoread`.
            ---@type boolean
            enabled = false,
            -- The name of the generated file. See `location` for more information.
            -- /!\ deprecated /!\ use `pathToFile` instead.
            ---@type string
            ---@example: `no-neck-pain-left.norg`
            ---@deprecated: use `pathToFile` instead.
            fileName = "no-neck-pain",
            -- By default, files are saved at the same location as the current Neovim session.
            -- note: filetype is defaulted to `norg` (https://github.com/nvim-neorg/neorg), but can be changed in `buffers.bo.filetype` or |NoNeckPain.bufferOptions| for option scoped to the `left` and/or `right` buffer.
            -- /!\ deprecated /!\ use `pathToFile` instead.
            ---@type string?
            ---@example: `no-neck-pain-left.norg`
            ---@deprecated: use `pathToFile` instead.
            location = nil,
            -- The path to the file to save the scratchPad content to and load it in the buffer.
            ---@type string?
            ---@example: `~/notes.norg`
            pathToFile = "",
        },
        -- colors to apply to both side buffers, for buffer scopped options @see |NoNeckPain.bufferOptions|
        --- see |NoNeckPain.bufferOptionsColors|
        colors = {
            -- Hexadecimal color code to override the current background color of the buffer. (e.g. #24273A)
            -- Transparent backgrounds are supported by default.
            -- popular theme are supported by their name:
            -- - catppuccin-frappe
            -- - catppuccin-frappe-dark
            -- - catppuccin-latte
            -- - catppuccin-latte-dark
            -- - catppuccin-macchiato
            -- - catppuccin-macchiato-dark
            -- - catppuccin-mocha
            -- - catppuccin-mocha-dark
            -- - github-nvim-theme-dark
            -- - github-nvim-theme-dimmed
            -- - github-nvim-theme-light
            -- - rose-pine
            -- - rose-pine-dawn
            -- - rose-pine-moon
            -- - tokyonight-day
            -- - tokyonight-moon
            -- - tokyonight-night
            -- - tokyonight-storm
            ---@type string?
            background = nil,
            -- Brighten (positive) or darken (negative) the side buffers background color. Accepted values are [-1..1].
            ---@type integer
            blend = 0,
            -- Hexadecimal color code to override the current text color of the buffer. (e.g. #7480c2)
            ---@type string?
            text = nil,
        },
        -- Vim buffer-scoped options: any `vim.bo` options is accepted here.
        ---@see NoNeckPain.bufferOptionsBo `:h NoNeckPain.bufferOptionsBo`
        bo = {
            ---@type string
            filetype = "no-neck-pain",
            ---@type string
            buftype = "nofile",
            ---@type string
            bufhidden = "hide",
            ---@type boolean
            buflisted = false,
            ---@type boolean
            swapfile = false,
        },
        -- Vim window-scoped options: any `vim.wo` options is accepted here.
        ---@see NoNeckPain.bufferOptionsWo `:h NoNeckPain.bufferOptionsWo`
        wo = {
            ---@type boolean
            cursorline = false,
            ---@type boolean
            cursorcolumn = false,
            ---@type string
            colorcolumn = "0",
            ---@type boolean
            number = false,
            ---@type boolean
            relativenumber = false,
            ---@type boolean
            foldenable = false,
            ---@type boolean
            list = false,
            ---@type boolean
            wrap = true,
            ---@type boolean
            linebreak = true,
        },
        --- Options applied to the `left` buffer, options defined here overrides the `buffers` ones.
        ---@see NoNeckPain.bufferOptions `:h NoNeckPain.bufferOptions`
        left = NoNeckPain.bufferOptions,
        --- Options applied to the `right` buffer, options defined here overrides the `buffers` ones.
        ---@see NoNeckPain.bufferOptions `:h NoNeckPain.bufferOptions`
        right = NoNeckPain.bufferOptions,
    },
    -- Supported integrations that might clash with `no-neck-pain.nvim`'s behavior.
    --
    -- The key of each integration must be the filetype of the integration window.
    --
    -- The `position` is used when the plugin scans the layout in order to compute the width that should be added
    -- on each side. For example, if you were supposed to have a padding of 100 columns on each side, but an
    -- integration takes 42, only 58 will be added so your layout is still centered.
    --
    ---@type table
    integrations = {
        -- @link https://github.com/nvim-tree/nvim-tree.lua
        ---@type table
        NvimTree = {
            -- The position of the tree.
            ---@type "left"|"right"
            position = "left",
        },
        -- @link https://github.com/nvim-neo-tree/neo-tree.nvim
        ["neo-tree"] = {
            -- The position of the tree.
            ---@type "left"|"right"
            position = "left",
        },
        -- @link https://github.com/mbbill/undotree
        undotree = {
            -- The position of the tree.
            ---@type "left"|"right"
            position = "left",
        },
        -- @link https://github.com/nvim-neotest/neotest
        neotest = {
            -- The position of the tree.
            ---@type "right"
            position = "right",
        },
        -- @link https://github.com/rcarriga/nvim-dap-ui
        dap = {
            -- The position of the tree.
            ---@type "none"
            position = "none",
        },
        -- @link https://github.com/hedyhli/outline.nvim
        outline = {
            -- The position of the tree.
            ---@type "left"|"right"
            position = "right",
        },
        -- @link https://github.com/stevearc/aerial.nvim
        aerial = {
            -- The position of the tree.
            ---@type "left"|"right"
            position = "right",
        },
        -- @link https://github.com/stevearc/oil.nvim
        oil = {
            -- The position of the tree.
            ---@type "none"
            position = "none",
        },
        -- this is a generic field to hint no-neck-pain that you use a dashboard plugin.
        -- the filetypes of natively supported dashboards are listed below in the `filetypes` field.
        -- if a dashboard that you use isn't supported, either set `dashboard.filetype` to the expected file type, or open a pull-request with the edited list.
        dashboard = {
            -- When `true`, debounce will be applied to the init method, leaving time for the dashboard to open.
            enabled = false,
            -- if a dashboard that you use isn't supported, you can use this field to set a matching filetype.
            ---@type string[]|nil
            filetypes = { "dashboard", "alpha", "starter", "snacks" },
        },
    },
    --- Allows you to provide custom code to run before (pre) and after (post) no-neck-pain steps (e.g. enabling).
    --- See |NoNeckPain.callbacks|
    ---@type table
    callbacks = {
        -- Runs right before centering the buffer
        ---@type fun(state: { enabled: boolean, active_tab: number, tabs: number[], disabled_tabs: number[], previously_focused_win: number })|nil
        preEnable = nil,
        -- Runs right after the buffer is centered
        ---@type fun(state: { enabled: boolean, active_tab: number, tabs: number[], disabled_tabs: number[], previously_focused_win: number })|nil
        postEnable = nil,
        -- Runs right before toggling NoNeckPain off
        ---@type fun(state: { enabled: boolean, active_tab: number, tabs: number[], disabled_tabs: number[], previously_focused_win: number })|nil
        preDisable = nil,
        -- Runs right after NoNeckPain has been turned off
        ---@type fun(state: { enabled: boolean, active_tab: number, tabs: number[], disabled_tabs: number[], previously_focused_win: number })|nil
        postDisable = nil,
    },
})

--- NoNeckPain's buffer `vim.wo` options.
---@see window options `:h vim.wo`
---
---@type table
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
NoNeckPain.bufferOptionsWo = {
    ---@type boolean
    cursorline = false,
    ---@type boolean
    cursorcolumn = false,
    ---@type string
    colorcolumn = "0",
    ---@type boolean
    number = false,
    ---@type boolean
    relativenumber = false,
    ---@type boolean
    foldenable = false,
    ---@type boolean
    list = false,
    ---@type boolean
    wrap = true,
    ---@type boolean
    linebreak = true,
}

--- NoNeckPain's buffer `vim.bo` options.
---@see buffer options `:h vim.bo`
---
---@type table
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
NoNeckPain.bufferOptionsBo = {
    ---@type string
    filetype = "no-neck-pain",
    ---@type string
    buftype = "nofile",
    ---@type string
    bufhidden = "hide",
    ---@type boolean
    buflisted = false,
    ---@type boolean
    swapfile = false,
}

--- NoNeckPain's scratchPad buffer options.
---
--- Leverages the side buffers as notepads, which work like any Neovim buffer and automatically saves its content at the given `location`.
--- note: quitting an unsaved scratchPad buffer is non-blocking, and the content is still saved.
---
---@type table
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
NoNeckPain.bufferOptionsScratchPad = {
    -- When `true`, automatically sets the following options to the side buffers:
    -- - `autowriteall`
    -- - `autoread`.
    ---@type boolean
    enabled = false,
    -- The name of the generated file. See `location` for more information.
    -- /!\ deprecated /!\ use `pathToFile` instead.
    ---@type string
    ---@example: `no-neck-pain-left.norg`
    ---@deprecated: use `pathToFile` instead.
    fileName = "no-neck-pain",
    -- By default, files are saved at the same location as the current Neovim session.
    -- note: filetype is defaulted to `norg` (https://github.com/nvim-neorg/neorg), but can be changed in `buffers.bo.filetype` or |NoNeckPain.bufferOptions| for option scoped to the `left` and/or `right` buffer.
    -- /!\ deprecated /!\ use `pathToFile` instead.
    ---@type string?
    ---@example: `no-neck-pain-left.norg`
    ---@deprecated: use `pathToFile` instead.
    location = nil,
    -- The path to the file to save the scratchPad content to and load it in the buffer.
    ---@type string?
    ---@example: `~/notes.norg`
    pathToFile = "",
}

--- NoNeckPain's buffer color options.
---
---@type table
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
NoNeckPain.bufferOptionsColors = {
    -- Hexadecimal color code to override the current background color of the buffer. (e.g. #24273A)
    -- Transparent backgrounds are supported by default.
    -- popular theme are supported by their name:
    -- - catppuccin-frappe
    -- - catppuccin-frappe-dark
    -- - catppuccin-latte
    -- - catppuccin-latte-dark
    -- - catppuccin-macchiato
    -- - catppuccin-macchiato-dark
    -- - catppuccin-mocha
    -- - catppuccin-mocha-dark
    -- - github-nvim-theme-dark
    -- - github-nvim-theme-dimmed
    -- - github-nvim-theme-light
    -- - rose-pine
    -- - rose-pine-dawn
    -- - rose-pine-moon
    -- - tokyonight-day
    -- - tokyonight-moon
    -- - tokyonight-night
    -- - tokyonight-storm
    ---@type string?
    background = nil,
    -- Brighten (positive) or darken (negative) the side buffers background color. Accepted values are [-1..1].
    ---@type integer
    blend = 0,
    -- Hexadecimal color code to override the current text color of the buffer. (e.g. #7480c2)
    ---@type string?
    text = nil,
}

--- NoNeckPain's buffer side buffer option.
---
---@type table
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
NoNeckPain.bufferOptions = {
    -- When `false`, the buffer won't be created.
    ---@type boolean
    enabled = true,
    ---@see NoNeckPain.bufferOptionsColors `:h NoNeckPain.bufferOptionsColors`
    colors = NoNeckPain.bufferOptionsColors,
    ---@see NoNeckPain.bufferOptionsBo `:h NoNeckPain.bufferOptionsBo`
    bo = NoNeckPain.bufferOptionsBo,
    ---@see NoNeckPain.bufferOptionsWo `:h NoNeckPain.bufferOptionsWo`
    wo = NoNeckPain.bufferOptionsWo,
    ---@see NoNeckPain.bufferOptionsScratchPad `:h NoNeckPain.bufferOptionsScratchPad`
    scratchPad = NoNeckPain.bufferOptionsScratchPad,
}

🧰 Commands

Command Description
:NoNeckPain Toggles the plugin state, between enable and disable.
:NoNeckPainResize INT Updates the config width with the given INT value and resizes the no-neck-pain windows.
:NoNeckPainToggleLeftSide Toggles the left side buffer (open/close).
:NoNeckPainToggleRightSide Toggles the right side buffer (open/close).
:NoNeckPainWidthUp Increases the config width by 5 and resizes the no-neck-pain windows.
:NoNeckPainWidthDown Decreases the config width by 5 and resizes the no-neck-pain windows.
:NoNeckPainScratchPad Uses the side buffers as a persistent scratchpad so you can take notes easily.
:NoNeckPainDebug Toggles the debug mode.

🏗 breaking changes

v1.0.0

See the release description for the full list of breaking changes.

v2.0.0

See the release description for the full list of breaking changes.

v3.0.0

See the release description for the full list of breaking changes.

🤖 For AI Assistants

This section provides structured information about the codebase to help AI assistants understand and work with the plugin effectively.

📁 Codebase Structure

Core Modules (lua/no-neck-pain/):

  • init.lua - Public API entry point, exports main commands (toggle(), resize(), etc.)
  • main.lua - Core plugin logic, orchestrates enable/disable/toggle operations
  • state.lua - Global state management (plugin enabled, tabs, windows)
  • config.lua - Configuration validation and defaults
  • ui.lua - Window/buffer creation and manipulation
  • colors.lua - Color/theme management for side buffers

Utilities (lua/no-neck-pain/util/):

  • api.lua - Debouncing and API utilities
  • helpers.lua - Common helper functions and safe state/config access
  • constants.lua - Plugin constants (supported integrations, filetypes)
  • log.lua - Debug logging

Tests (tests/):

  • test_*.lua - Feature-specific test suites
  • helpers.lua - Test utilities and fixtures

🔑 Key Entry Points

  1. Plugin initialization: lua/no-neck-pain/init.lua

    • Exports: NoNeckPain.toggle(), NoNeckPain.resize(), NoNeckPain.toggle_scratch_pad()
  2. Core logic: lua/no-neck-pain/main.lua

    • Contains enable/disable/toggle implementations
  3. State management: lua/no-neck-pain/state.lua

    • Global state accessible via require("no-neck-pain.util.helpers")

⚙️ Configuration Structure

The plugin uses a deeply nested configuration object, see lua/no-neck-pain/config.lua

🔄 Common Patterns

  1. State and Config Access: Always use require("no-neck-pain.util.helpers") for state manipulation

    • get_config_field(key) - Read config values
    • merge_config(partial) - Update config
    • get_tab() - Get current tab state
  2. Event Handling: Plugin responds to autocmds (VimEnter, TabEnter, ColorScheme, etc.)

  3. Side Buffer Management: Creates "padding" buffers on left/right sides

    • Named no-neck-pain-left and no-neck-pain-right when setNames = true
    • Filetype: no-neck-pain (or custom via buffers.bo.filetype)
  4. Integration Support: Detects file trees (NvimTree, neo-tree) and dashboards

    • Adjusts width calculations to account for sidebar positions

🧪 Testing

  • Test framework: MiniTest
  • Run tests: make test
  • Test pattern: Each feature has dedicated test_*.lua file
  • Helpers: tests/helpers.lua provides utilities for test setup/teardown

⌨ Contributing

PRs and issues are always welcome. Make sure to provide as much context as possible when opening one.

See Makefile for the available commands

It's recommended to use Bob, a useful nvim version manager in order to run the test suite for every supported versions.

🗞 Wiki

You can find guides and showcase of the plugin on the Wiki

🎭 Motivations

Although there's other (amazing!) alternatives that provide a zen-distraction-free-center mode, they usually make assumptions that might alter your workflow, or at least require some configuration to suit your needs.

no-neck-pain.nvim aims at providing a seamless non-opinionated buffer centering experience, while being super customizable.

About

☕ Dead simple yet super extensible zen mode plugin to protect your neck.

Topics

Resources

Stars

966 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages