jaimeibanezrivera/zj-theme

github github
terminal-integration
stars 3
issues 0
subscribers 0
forks 0
CREATED

UPDATED


zj-theme.nvim

CI

Sync your zellij theme to whatever colorscheme is active in neovim.

Demo: switching nvim colorscheme through catppuccin, gruvbox-material, tokyonight, PaperColor, and carbonfox, with zellij's theme following live

It rewrites zellij's config file directly. zellij watches that file for changes and applies them to your already-running session automatically (zellij polls it roughly once a second), so this takes effect live — no restart needed.

Requires Neovim >= 0.10 (for the vim.health.* API used by :checkhealth). See also :help zj-theme.

How it works

  1. A ColorScheme autocmd fires whenever you run :colorscheme ... (or a plugin sets one on your behalf).
  2. The new vim.g.colors_name is looked up in a mapping table (built-in defaults + anything you add) to find the corresponding zellij theme name.
  3. The theme "..." line in zellij's config.kdl is rewritten in place with that theme name.
  4. If any step fails — the colorscheme has no mapping, the config file isn't readable, or it has no theme line to rewrite — it falls back to a configurable default zellij theme instead of leaving things inconsistent.

Installation

No external plugin dependency needed — zj-theme.nvim syncs every pane in the session, including the one nvim itself runs in, on its own via the zellij CLI. See Pane backgrounds for how that works.

lazy.nvim

-- For `plugins/zj-theme.lua` users.
return {
  "jaimeibanezrivera/zj-theme",
  lazy = false,
  config = function()
    require("zj-theme").setup({
      -- see Configuration below
    })
  end,
}
-- For `plugins.lua` users.
{
  "jaimeibanezrivera/zj-theme",
  lazy = false,
  config = function()
    require("zj-theme").setup({
      -- see Configuration below
    })
  end,
}

packer.nvim

use({
  "jaimeibanezrivera/zj-theme",
  config = function()
    require("zj-theme").setup({
      -- see Configuration below
    })
  end,
})

vim-plug

Plug 'jaimeibanezrivera/zj-theme'

Then, elsewhere in your init.vim/init.lua (vim-plug has no per-plugin config callback):

lua require("zj-theme").setup({
  -- see Configuration below
})

mini.deps

local MiniDeps = require("mini.deps")

MiniDeps.add({ source = "jaimeibanezrivera/zj-theme" })

MiniDeps.now(function()
  require("zj-theme").setup({
    -- see Configuration below
  })
end)

vim.pack (Neovim >= 0.12, built in, no plugin manager needed)

vim.pack.add({
  "https://github.com/jaimeibanezrivera/zj-theme",
})

require("zj-theme").setup({
  -- see Configuration below
})

Local development (no plugin manager, this repo checked out on disk)

{
  dir = "~/path/to/zj-theme.nvim",
  config = function()
    require("zj-theme").setup({
      -- see Configuration below
    })
  end,
}

Configuration

require("zj-theme").setup({
  -- Path to zellij's config.kdl. Defaults to ~/.config/zellij/config.kdl.
  zellij_config_path = vim.fn.expand("~/.config/zellij/config.kdl"),

  -- zellij themes to fall back to when the current colorscheme has no
  -- mapping, or when writing zellij_config_path fails. Picked based on
  -- vim.o.background so a dark colorscheme doesn't fall back to a light
  -- zellij theme or vice versa.
  default_dark_theme = "default",
  default_light_theme = "pencil-light",

  -- nvim colors_name -> zellij theme name. Merged over the built-in table
  -- in lua/zj-theme/mappings.lua, so you only need overrides/additions.
  -- A value can also be { dark = "...", light = "..." } for colorschemes
  -- that keep vim.g.colors_name the same across light/dark (ayu.nvim,
  -- gruvbox-material, everforest-nvim all do this, and are already handled
  -- by the bundled defaults) — resolved via the current vim.o.background.
  mappings = {
    mycustomtheme = "my-zellij-theme-name",
    myflexibletheme = { dark = "my-dark-zellij-theme", light = "my-light-zellij-theme" },
  },

  -- Whether to push the current colorscheme's bg/fg to every pane in the
  -- zellij session, including the one nvim itself is running in. See
  -- Pane backgrounds below. Set to false to leave every pane's background
  -- alone.
  sync_pane_backgrounds = true,

  -- How often (ms) to poll for newly created zellij panes and color them
  -- to match the current colorscheme — otherwise a pane opened after a
  -- `:colorscheme` switch keeps zellij's default background until the next
  -- one. Set to 0 to disable polling. Depends on the sync_pane_backgrounds
  -- option. Not recommended to lower the number below the default value.
  pane_poll_interval_ms = 1000,

  -- Set to false to silence vim.notify warnings/errors.
  notify = true,
})

Your config.kdl needs an actual (uncommented) theme "..." line already present for this plugin to find and rewrite — it won't add one from scratch.

On the zellij side

Whatever "my-zellij-theme-name" you map a colorscheme to in mappings has to actually exist for zellij, or nothing changes color once written. Two ways to make that true:

  • It's one of zellij's built-in themes — nothing further needed, this is what Supported colorschemes above uses.
  • It's a theme you've defined yourself, in a themes { "my-zellij-theme-name" { ... } } block in config.kdl (or a separate file in zellij's theme_dir) — see zellij's theme documentation for the format.

This plugin only rewrites the theme "..." line; it doesn't check whether that name resolves to anything on zellij's side. Map to a theme that doesn't exist (a typo, or one you meant to define but didn't) and zellij will just silently fail to apply it — nothing will look wrong here, the line will be written correctly, it just won't do anything.

Manual trigger

:ZjThemeNow re-applies the mapping for whatever colorscheme is currently active — useful for testing config changes without switching colorschemes.

Pane backgrounds

Rewriting zellij's theme only changes zellij's own colors — borders, tab bar, status bar. It doesn't touch the background you actually see inside each pane. zj-theme.nvim handles that itself, controlled by the sync_pane_backgrounds option:

Every pane in the session, including the one nvim itself is running in, gets recolored using a zellij command that can set a pane's background directly (needs zellij 0.44 or newer). This is a property zellij holds for that pane, not something tied to nvim's own process, which is why it sticks around after nvim exits. This needs the zellij command to actually be available on your system, not just a zellij session to be running — :checkhealth zj-theme will tell you if it isn't.

Colors are never reset back to zellij's own defaults — once a pane is colored, it stays that way, including after nvim exits, until the next :colorscheme switch recolors it again. The goal is a session that keeps looking like your last active colorscheme everywhere, not just while nvim happens to be running in one of its panes.

Set sync_pane_backgrounds = false to turn this off.

Demo: a pane split next to nvim, and a second tab, both recoloring live as nvim's colorscheme cycles through nord, dracula, kanagawa, catppuccin-latte, catppuccin-mocha, and everforest

New panes

Panes only get recolored when you change colorscheme or start nvim, so a pane you open afterwards will briefly show zellij's default background until it catches up. To smooth that over, a quiet background check runs about once a second (pane_poll_interval_ms) looking for panes it hasn't colored yet. Set pane_poll_interval_ms = 0 if you'd rather turn that off.

Getting rid of that brief delay entirely would take a small companion living inside zellij itself, reacting the moment a pane appears instead of checking every second or so — see Roadmap below.

My advice: make your terminal background as see-through as possible. That way the color swap won't feel so harsh. Your terminal's real background still shows through between the pane lines anyway, so it just looks better with lower opacity in general. Bonus: if you switch between light and dark mode with different wallpapers in your OS, this helps there too.

Supported colorschemes

Best-effort mappings for these plugins are bundled by default — see lua/zj-theme/mappings.lua for the exact zellij theme each one resolves to, and override any of them via mappings in setup().

Plugin Colorscheme names
catppuccin/nvim catppuccin, catppuccin-mocha, catppuccin-macchiato, catppuccin-frappe, catppuccin-latte
ellisonleao/gruvbox.nvim gruvbox
sainnhe/gruvbox-material gruvbox-material
folke/tokyonight.nvim tokyonight, tokyonight-night, tokyonight-storm, tokyonight-moon, tokyonight-day
EdenEast/nightfox.nvim nightfox, dayfox, carbonfox
Shatur/neovim-ayu ayu (ayu-dark/ayu-light/ayu-mirage are also mapped, for other ayu-family plugins that set those names directly)
maxmx03/solarized.nvim solarized
lifepillar/vim-solarized8 solarized8, solarized8_flat, solarized8_high, solarized8_low
navarasu/onedark.nvim onedark
sonph/onehalf onehalfdark (onehalflight has no zellij light-theme equivalent)
gbprod/nord.nvim nord
Mofiqul/dracula.nvim dracula
rebelot/kanagawa.nvim kanagawa
neanias/everforest-nvim everforest
NLKNguyen/papercolor-theme PaperColor

Using something else? Add it via mappings in setup() — see Configuration.

Fallback behavior

default_dark_theme or default_light_theme (chosen by vim.o.background) is used whenever:

  • nvim is not running inside a zellij session ($ZELLIJ unset) — no-op, nothing is written at all.
  • the active colorscheme has no entry in mappings.
  • zellij_config_path doesn't exist/isn't readable, or has no theme "..." line to rewrite.

Health

Run :checkhealth zj-theme to check whether setup() has been called, whether you're currently inside a zellij session, whether zellij_config_path exists and has a theme "..." line, whether the active colorscheme is mapped, and — for Pane backgrounds — whether sync_pane_backgrounds is active, plus whether the zellij CLI is on PATH and its version is new enough for set-pane-color.

Recommended plugins

  • zellij-nav.nvim — seamless navigation between nvim splits and zellij panes, so the same keys move focus across both. Complements this plugin: that one syncs navigation, this one syncs the theme.

Roadmap

  • A zellij-side companion plugin. The polling workaround for newly opened panes works, but it's a temporary fix — a real fix would be a small plugin running inside zellij itself (via the zellij-tile WASM plugin API) that reacts to a pane appearing instead of polling. Nothing exists for this yet, but it's on my list to eventually build.
  • Open to other ideas. If you've got a suggestion beyond the zellij-side companion above, open an issue — happy to hear it out.

Contributing

Want to dig into the code or send a PR? See ARCHITECTURE.md for a developer-facing walkthrough of how the plugin is put together — the module breakdown, the event/data flow through setup() and apply_all(), and diagrams for each of the two sync mechanisms (zellij's own theme, every pane's background).

License

MIT, see LICENSE.