README.md

November 25, 2025 Β· View on GitHub

witch-line

The best statusline plugin for neovim. It's very lightweight and super fast.

This plugin lazy load as much as possible

A few words to say

πŸŽ‰ The default component is written for my personal use. So maybe you need to create your own component. I'm very happy to see your component. So if you have any idea to create a new component, please open an issue or pull request.

Concept ideas

  • I like the reference concept in any database structure based on id. So I use the same concept in this plugin for component system. You can reference other component by id to share some field like events, style, static, context, hidden, min_screen_width. This will help you to create a component based on other component without duplicate code.

  • Spoiler this plugin also provide nested tables to inherit from parent by recursively for anyone enjoy with creating a component based on other component by nested table like heirline. But I think the reference concept is better. And the statusline is a flat structure and readable.

Understand Concept

What's is the reference concept.

  • I assume that almost people know about heirline. It's a well-being statusline framework based on recursion with many nested tables to inherit the value. It's good. But to be honestly, i think it's quite redundant, and some time make the component biggest and hard to maintain. We always retain the deepest nested level is less than 3 for avoiding aweful behavior and hard to control. And almost popular component isn't necessary to create more than 2 level inheritance. So why not make some changes with a flatten component list. That's why reference concept appears.

Reference is not a new topic. You meet it in many cases such example: in database a document, a table reference to another by id. In rust we has borrowing, or in C/C++ we has pointer. And now, I move this concept to witch-line component.

See the magic:

  -- We move from
  -- heirline
  local Comp = {
    style = {
      fg= ...
    },
    {
      provider= ...
    },
    {
      provider= ...
    },
  }


  -- to witch-line
  -- You can see the difference and detail about ref field and inherit field in [COOKBOOK](./docs/COOKBOOK.md)
  local Parent = {
    id = "A"
    style = ...
  }
  local Child1 = {
    id = "B",
    ref = { -- ref particular field only
      style = "A"
    }
  }

  local Child2 = {
    id  = "C",
    inherit = "A"
  }



Preview

  • Basic style (No separator, I aprreciate basic but you can create it or do many things else by yourself (like add separator) or you can create new PR for your feature you wantc)
image
  • Individual statusline for each window.
image
  • Individual component value for each window.
image
  • Video:

https://github.com/user-attachments/assets/241d091f-bfdb-4935-b33d-8c8a2626c2a4

✨ Features

witch-line is a fast, lightweight, and fully customizable statusline plugin for Neovim. It focuses on modularity, caching, and performance. Below are the key features:

  • ⚑ Blazing Fast: Optimized with internal caching and minimal redraws to keep your statusline snappy and efficient. Just config for first time and every thing will be cache and run super fast later.

  • 🧩 Modular Components: Define reusable and nested components using a simple configuration format.

  • πŸŽ› Abstract Components: Support for abstract components that can be composed and reused without rendering directly.

  • 🎨 Flexible Layouts: Arrange statusline components in any order, across multiple layers or segments.

  • πŸ” Reactive Updates: Smart detection of buffer/file changes to update only when necessary.

  • πŸ“ Context-Aware Disabling: Automatically disable the statusline for specific filetypes or buftypes (e.g. terminal, help, etc).

  • 🧠 Config Hashing: Detect if user config has changed via xxh32 hashing, ensuring minimal reinitialization.

  • πŸ’Ύ Persistent Caching: Cache user configurations and state across sessions using a simple key-value system.

  • πŸ§ͺ Testable & Maintainable: Designed with testability and clear API boundaries in mind.

  • πŸ›  Extensible: Easily extend with custom components.

This plugin is ideal for developers who want full control over the look and feel of their statusline, without sacrificing performance or flexibility.


TODO

  • Laststatus

    • Support for laststatus = 1
    • Support for laststatus = 2
    • Support for laststatus = 3
    • Support for laststatus = 0
  • Cache

    • Implement caching mechanism (serialization + deserialization)
    • Cache all needed data
    • Use checksum to detect config changes with xxh32
    • Lazy compile function of component
    • Detect default component changed automatically when plugin was updated
    • Support up-value for component function caching
    • Support paritial cache loading
  • Customization

    • Support user-defined component
    • Support override default component by user value
  • Component

    • Only update component when needed
    • Implement component system
    • Support abstract component
    • Support nested component
    • Support ref field to reference other component
    • Support inherit field to inherit from other component
    • Support static field to store static data
    • Support context field to store context data
    • Support events field to trigger component update
    • Support timing field to update component periodically
    • Support lazy field to lazy load component
    • Support padding field to add padding around component
    • Support style field to override component style
    • Support left_style field to override left part style
    • Support right_style field to override right part style
    • Support left field to add left content
    • Support right field to add right content
    • Support min_screen_width field to hide component if screen width is less than this value
    • Support hidden field to hide component based on condition
    • Support init function to initialize component
    • Support pre_update function to run before update function
    • Support post_update function to run after update function
    • Support update function to generate component content
    • Support ref field to reference other component fields (events, style, static, context, hidden, min_screen_width)
    • Support version field to manage component cache
    • Support flexible field to hide component based on priority when space is limited
    • Support on_click function to handle click events
    • Support win_individual field to enable individual value for each window
    • Support coroutine for update function
  • Hide Automatically

    • Implement disable system
    • Support disable for specific filetypes
    • Support disable for specific buftypes
    • Support for laststatus = 1, 2
  • Commands

    • Implement :Witchline clear_cache command to clear cache
    • Implement :Witchline inspect command to inspect some information
  • Testing

    • Write unit tests for core functionality
    • Write performance benchmarks
  • Themes

    • Auto adjust color of components based on theme
  • Bug fixs (Will fix soon this important)

    • A probably inheritance logic when conflicting between parent and child happens

Compare with other statusline plugins


Benchmarks

The benchmarks of 30 runs:

This is just an example because I don't have much time to make each statusline with the same ui. And i'm not sure is it the best config for each statusline.

I just run with the default config of each plugin.

If some one remake your current statusline with "witch-line", could you send me the config of each statusline to make it properly?

The benchmarks is run with nvim -u with only the statusline plugin loaded.

PluginLoad Time (ms)Avg Update Time (ms)
witch-line3.4137 (cached: 2.22)
lualine4.4964
heirline6.3693

Installation

    -- lazy
    {
        "sontungexpt/witch-line",
        dependencies = {
            "nvim-tree/nvim-web-devicons",
        },
        lazy = false, -- Almost component is lazy load by default. So you can set lazy to false
        opts = {},
    },

Usage

Laststatus

You should set laststatus by yourself. I recommend you set laststatus to 3 to be better.

vim.o.laststatus = 3

Options

You can setup the plugin by calling the setup function and passing in a table of options.

require("witch-line").setup({
  --- @type CombinedComponent[]
  abstracts = {
    "file.name",
    {
      id = "file", -- Abstract component for file-related info
      padding = { left = 1, right = 1 }, -- Padding around the component
      static = { some_key = "some_value" }, -- Static metadata
      style = { fg = "#ffffff", bg = "#000000", bold = true }, -- Style override
      min_screen_width = 80,          -- Hide if screen width < 80
    },
  },

  --- @type CombinedComponent[]
  statusline = {
    --- The global statusline components
    --- Set it to `nil` if you want to use default components in example
    global = {
        "mode",
        "file.name",
        "git.branch",
        {
          id = "component_id",               -- Unique identifier
          padding = { left = 1, right = 1 }, -- Padding around the component
          static = { some_key = "some_value" }, -- Static metadata
          win_individual = false,
          timing = false,                 -- No timing updates
          style = { fg = "#ffffff", bg = "#000000", bold = true }, -- Style override
          min_screen_width = 80,          -- Hide if screen width < 80
          hidden = function()               -- Hide condition
            return vim.bo.buftype == "nofile"
          end,
          left_style = { fg = "#ff0000" }, -- Left style override
          update = function(self, ctx, static, session_id) -- Main content generator
            return vim.fn.expand("%:t")
          end,
          ref = {                       -- References to other components
            events = { "file.name" },
            style = "file.name",
            static = "file.name",
          },
        },
    },

    -- @type fun(winid): CombinedComponent[]|nil
    win = nil
  },

  cache = {
      -- You can disable cache here.
      -- If you enable cache you can not use any up-value in your component functions otherwise your
      -- cache will be broken.
      enabled = true,
      -- Show notification when cache is cleared. Default true.
      notification = true,
      -- Strip debug info when caching dumped functions. Default false. Faster but harder to debug.
      func_strip = false,
  },

  disabled = {
    filetypes = { "help", "TelescopePrompt" },
    buftypes = { "nofile", "terminal" },
  },

  --- Whether to automatically adjust the theme.
  --- If it is set to false the `auto_theme` field of the component will be ignored.
  --- Default: true.
  --- You can toggle it by `:Witchline toggle_auto_theme`
  auto_theme = true

})

Top level options

FieldTypeDescription
abstractsCombinedComponent[]A list of abstract components registered before everything else. Used for component references and dependency resolution.
statusline{ global: CombinedComponent[], win?: fun(winid): CombinedComponent[] }Defines the global statusline and optional per-window statusline overrides.
cache{ full_scan: boolean, notification: boolean, func_strip: boolean }Cache behavior and optimizations.
disabled{ filetypes: string[], buftypes: string[] }Filetypes/buftypes where the plugin should be disabled.

statusline

KeyTypeDescription
globalCombinedComponent[]|nilGlobal statusline components. Set to nil if you want to use default components in example.
winfun(winid): CombinedComponent[]Per-window statusline components. When using this field, you must set laststatus to 2 or 1, and you must add all neccesary components to the abstracts field to let it's work.

Example config using win option

require("witch-line").setup({
    abstracts = {
        "battery", -- pre register battery to use in win option
        -- require("your custom component")
    }
    statusline = {
        global = {
            "file.name",
            "git.branch",
            --- require("your custom component")
            --- Other components
        }
        win = function(winid
          --- Only show battery in NvimTree window
          local filetype = vim.bo[vim.api.nvim_win_get_buf(winid)].filetype
          if filetype == "NvimTree" then
            return {
                "battery",
                -- require("your custom component")
            }
          end
        end
    }

})

cache

KeyTypeDefaultDescription
enabledbooleantrueEnable caching.
notificationbooleantrueShows a notification when the cache is cleared.
func_stripbooleanfalseStrips debug info from dumped functions to reduce cache size.

disabled

KeyTypeDescription
filetypesstring[]Filetypes where the plugin should be disabled.
buftypesstring[]Buftypes where the plugin should be disabled.

Commands

The plugin provides the following commands:

  • :Witchline clear_cache - Clear the plugin's cache.
  • :Witchline inspect - Use for debugging and inspecting internal state.
  • :Witchline toggle_auto_theme - Toggle automatic theme adjustment.

🧾 Default Components Reference

This section describes the built-in components available in the plugin, their structure, and how to use them. Each component is referenced by name and can be composed to build a flexible and performant statusline.


πŸ”– Default Components

NameModule FileDescription
modemode.luaShows the current Neovim mode
file.namefile.luaDisplays the filename
file.iconfile.luaDisplays an icon for the file
file.modifierfile.luaIndicates if the file has unsaved changes
file.sizefile.luaShows the file size
%=(builtin)Separator to align left/right components
copilotai/copilot.luaShows Copilot status (if available)
windsurfai/windsurf.luaShows Codeium status (if available)
windsurf.neocodeiumai/windsurf.luaShows Neocodium status (if available)
diagnostic.errordiagnostic.luaShows number of error diagnostics
diagnostic.warndiagnostic.luaShows number of warning diagnostics
diagnostic.infodiagnostic.luaShows number of info diagnostics
diagnostic.hintdiagnostic.luaShows number of hint diagnostics
encodingencoding.luaDisplays file encoding (e.g., utf-8)
cursor.poscursor.luaShows the current cursor line/column
cursor.progresscursor.luaShows the cursor position as a % progress
lsp.clientslsp.luaLists active LSP clients
git.branchgit.luaShows current Git branch
git.diff.addedgit.luaNumber of added lines in Git diff
git.diff.removedgit.luaNumber of removed lines in Git diff
git.diff.modifiedgit.luaNumber of changed lines in Git diff
datetimedatetime.luaDisplays current date and time
batterybattery.luaShows battery status (if applicable)
os_unameos_uname.luaDisplays the operating system name
nvim_dapnvim_dap.luaShows nvim-dap status (if available)
search.countsearch.luaShows number of searching value
selection.countselection.luaShows number of selection zone

πŸ› οΈ Customizable Fields for Components

Each component accepts a set of customization fields to control its behavior, style, visibility, and layout.

Below is a table of all supported fields and their expected types:

FieldType(s)Description
paddingnumber, tableAdds padding around the component. Can be a single number or { left, right }.
staticanyAny static value or metadata the component wants to keep.
timingboolean, numberEnables timing or sets a custom update interval for the component.
stylefunction, tableStyle override for the entire component output (e.g., color, bold).
min_screen_widthnumberHides the component if the screen width is below this threshold.
hiddenfunction, booleanHide condition. If true or a function that returns true, hides the component.
left_stylefunction, tableStyle override applied to the left part of the component.
leftstring, functionLeft content to be rendered. Can be a string or a generator function.
right_stylefunction, tableStyle override applied to the right part of the component.
rightstring, functionRight content to be rendered. Can be a string or a generator function.
flexiblenumberPriority for hiding when space is limited. Lower numbers hide first.

You can use the require("witch-line.builtin").comp builtin function to create a customized version of any default component by specifying overrides for these fields.


local my_component = require("witch-line.builtin").comp("file.name", {
  padding = { left = 2 },
  min_screen_width = 60,
  hidden = function()
    return vim.bo.buftype == "nofile"
  end,
  style = { fg = "#ffffff", bg = "#222222", bold = true },
})

Or you can also use the [0] field to override the default component.

local my_component = {
  [0] = "file.name",  -- Inherit from the default file.name component
  padding = { left = 2 },
  min_screen_width = 60,
  hidden = function()
    return vim.bo.buftype == "nofile"
  end,
  style = { fg = "#ffffff", bg = "#222222", bold = true },
}

🧠 Component Structure

Each component is represented as a Lua table with various fields that define its behavior, appearance, and interactions. You can read the COOKBOOK for more examples of component structure.

πŸ“š Public APIs

The plugin exposes a set of public APIs for advanced usage and customization. You can find the API reference in the API.md file.

🀝 Community Help & Contributions Wanted

witch-line is a flexible and powerful statusline plugin for Neovim, but there's still a lot of room to improve and grow. I'm actively seeking help and contributions from the community to make this project even better.

Here are a few areas where your help would be especially appreciated:

  • πŸ“˜ API Documentation Help rewrite and polish the API reference into clear and professional documentation. Better docs will make it easier for others to build powerful custom setups.

  • 🧬 Serialization System Design and implement a robust system to serialize and deserialize component configurations. This would help cache system work better.

  • πŸ§ͺ Component Testing Framework Improve or design an ergonomic and declarative way to test components individually and ensure they behave consistently in different contexts.

  • πŸ“¦ Plugin Ecosystem You can create new plugin extensions built on top of witch-lineβ€”such as battery indicators, LSP diagnostics, Git integrations, and more.

  • πŸ’‘ Ideas, Feedback, and Bug Reports Even if you’re not a coder, suggestions, feedback, and bug reports are very welcome.

If you’re interested in helping, feel free to open an issue, start a discussion, or submit a PR. Let's build something awesome together. πŸ™

πŸ“œ License

This project is licensed under the MIT License - see the LICENSE file for details