omarchy icon indicating copy to clipboard operation
omarchy copied to clipboard

Auto-load themes for neovim

Open tahayvr opened this issue 5 months ago • 29 comments

This PR adds an automatic reload feature for NeoVim themes when you change your Omarchy theme.

https://github.com/user-attachments/assets/bc552ab4-6733-4687-90d2-2127504fbae1

  • It uses a new LazyVim starter repo that includes the required config for this solution to work.

Omarchy lazyvim starter repo (feel free to fork this repo into your own github and switch the git clone command in install/development/nvim.sh)

  • I also needed to add a specific flavour for the regular Catppuccin theme (mocha) for this to work as intended.
  • Finally, I added a line in the omarchy-theme-set that triggers the load of the new theme in NeoVim.

How does the automatic reload feature work?:

In order to enable the automatic reload feature, each theme in Omarchy must have a neovim.lua file that defines the themes like this:

-- .config/omarchy/themes/gruvbox/neovim.lua
return {
    { "morhetz/gruvbox", lazy = false },
    {
        "LazyVim/LazyVim",
        opts = {
            colorscheme = "gruvbox",
        },
    },
}

Additionally, we must make sure that LazyVim is configured to load every theme on startup. This is done by setting the spec and install options in the LazyVim configuration.

-- .config/nvim/lua/config/lazy.lua
spec = {
        -- add LazyVim and import its plugins
        { "LazyVim/LazyVim",           import = "lazyvim.plugins" },
        -- import/override with your plugins
        { import = "plugins" },
        -- import themes on load
        { "ellisonleao/gruvbox.nvim", lazy =false },
        { "catppuccin/nvim",           name = "catppuccin", lazy = false },
        { "folke/tokyonight.nvim", lazy = false },
        { "neanias/everforest-nvim", lazy = false },
        { "rebelot/kanagawa.nvim", lazy = false },
        { "tahayvr/matteblack.nvim", lazy = false },
        { "EdenEast/nightfox.nvim", lazy = false },
        { "rose-pine/neovim",          name = "rose-pine", lazy = false },
    },
install = { colorscheme = { "catppuccin-latte", "catppuccin-mocha", "tokyonight", "habamax", "gruvbox", "everforest", "kanagawa", "nordfox", "matteblack", "rose-pine-dawn" },

Then, we need a function to reload the theme based on Omarchy's current theme symlink target. This function will check if the theme has changed and apply the new theme without restarting Neovim.

-- .config/nvim/lua/init.lua

-- Variable to store the last known theme symlink target
local last_theme_target = nil

-- Function to reload the theme based on Omarchy's current theme
function ReloadTheme()
    local theme_dir = vim.fn.resolve(vim.fn.expand('~/.config/omarchy/current/theme'))
    local theme_config = theme_dir .. '/neovim.lua'

    -- Check if the theme directory has changed
    if theme_dir == last_theme_target then
        return -- No change, skip reload
    end

    if vim.fn.filereadable(theme_config) == 1 then
        local ok, result = pcall(dofile, theme_config)
        if not ok then
            print("Error loading theme: " .. result)
            return
        end
        -- Check if result is a table
        if type(result) == "table" then
            -- Look for LazyVim opts with colorscheme
            for _, entry in ipairs(result) do
                if entry[1] == "LazyVim/LazyVim" and entry.opts and entry.opts.colorscheme then
                    local ok, err = pcall(vim.cmd, 'colorscheme ' .. entry.opts.colorscheme)
                    if not ok then
                        print("Error applying colorscheme: " .. err)
                        return
                    end
                    vim.cmd('redraw!')
                    last_theme_target = theme_dir -- Update last known target
                    return
                end
            end
            print("No valid colorscheme found in " .. theme_config)
        else
            print("Invalid theme config format in " .. theme_config)
        end
    else
        print("Theme config not found: " .. theme_config)
    end
end

-- Load the theme on startup
ReloadTheme()

How does Neovim know when to reload the theme?: We can use a signal handler that listens for the SIGUSR1 signal. When this signal is received, it will trigger the ReloadTheme function.

-- .config/nvim/lua/init.lua

-- Reload theme on SIGUSR1 signal
vim.api.nvim_create_autocmd('User', {
    pattern = 'OmarchyThemeReload',
    callback = function()
        ReloadTheme()
    end,
})

-- Set up SIGUSR1 handler using vim.loop
local uv = vim.loop
local signal = uv.new_signal()
uv.signal_start(signal, "sigusr1", function()
    vim.schedule(function()
        vim.cmd('doautocmd User OmarchyThemeReload')
    end)
end)

and finally, we need to send the SIGUSR1 signal to Neovim when the theme is changed. This can be done using a shell command or a script that changes the theme symlink and sends the signal to Neovim.

# omarchy-theme-set.sh

# Send SIGUSR1 signal to Neovim
pkill -SIGUSR1 nvim 2>/dev/null || true
```

tahayvr avatar Jul 26 '25 00:07 tahayvr

Awesome! Been looking for this. I'm wondering if there isn't a way for us to upstream the function to do live theme changes to LazyVim directly? I'd rather not maintain a fork if we can get away with not doing that. Alternatively, we could put that function into our own LazyVim checkout, but still basing it on their package. Might that be possible?

dhh avatar Jul 26 '25 14:07 dhh

I'll investigate.

tahayvr avatar Jul 26 '25 18:07 tahayvr

@dhh We are really just changing two files in the starter: .config/nvim/lua/config/lazy.lua .config/nvim/lua/init.lua

what if we have these two modified files in a lazyvim folder in our repo: config/lazyvim

then copy them as part of the nvim.sh script ( install/development/nvim.sh ) to replace the default files cloned from the official starter.

` cp ~/.local/share/omarchy/config/lazyvim/init.lua ~/.config/nvim/lua/

cp ~/.local/share/omarchy/config/lazyvim/lazy.lua ~/.config/nvim/lua/config/ `

Sound better?

tahayvr avatar Jul 26 '25 19:07 tahayvr

I changed the starter repo back to the original, and implemented the copy method mentioned above.

tahayvr avatar Jul 26 '25 20:07 tahayvr

Sorry - I'm not super familiar with omarchy's internals, but I had a few questions:

  1. Any particular reason this is added to the lazyvim folder instead of the nvim folder in config? A lot of this looks like it could be loaded via a plugin/ file in that folder
  2. I would recommend having a way for users to turn this off, something like checking vim.g.omarchy_autochange_theme or something, so that users can disable in their configs if they don't want this behavior
  3. perhaps better than trying to load and install all the colorschemes here, you could potentially just try pcall'ing the colorscheme command to the name of the omarchy colorscheme and see if it's installed (possibly, with a way of installing default themes as well for omarchy)?

tjdevries avatar Aug 04 '25 11:08 tjdevries

Based on comments from @tjdevries I refactored the code to separate the function and the themes into an Omarchy folder in nvim/lua. So we're not touching LazyVim files anymore.

Thanks TJ for the review. As for the installation of colorschemes, It's the only way [I found] to get a theme refresh to work without hiccups. The user now doesn't have to manually initiate an install or load for a colorscheme (at least for the default themes).

@dhh this separation of code to omarchy's own folder also helps us figure out a way to add other themes to the lua/omarchy/init.lua file when the user installs third party themes.

Let me know what you think.

tahayvr avatar Aug 06 '25 08:08 tahayvr

How does this work if the user installs an extra theme? Then it does just what we're doing now the first time? But after that it'll work?

dhh avatar Aug 06 '25 14:08 dhh

How does this work if the user installs an extra theme? Then it does just what we're doing now the first time? But after that it'll work?

The nvim theme & colorscheme name must be added to the list in the lua/omarchy/init.lua file (we can automate this in the install theme script). Once it's there, Lazy will install it on first launch and it'll work after that.

The only issue is that if the user is using a theme and then modifying/ovverriding the values, it's not gonna use those modifications, it'll just use the base colorscheme.

tahayvr avatar Aug 06 '25 17:08 tahayvr

Hmm, wish we could make it work like before where it could delegate to the theme nvim, if one is present. And if not present, we can assume that the theme is present in the preconfigured list. I like really way to sed an existing file with extra theme installs in and out if we can avoid it.

dhh avatar Aug 06 '25 17:08 dhh

Something I played with a couple of days ago was that you can trigger changing themes by just require("tokyonight").load({ style = "storm" }) or require("matteblack").colorscheme(). If it's placed in your init.lua it'll flip to that theme at launch, and if it's added to a plugin file, it'll flip when it's updated due to LazyVim already having auto reloading on config changes.

There are some issues that I haven't worked out in here but in my head, there's some combination we should be able to achieve of like a symlinked .lua in the theme file that should be able to trigger the built-in LazyVim reloader and then we don't have to carry responsibility for so much of this. I believe the config will even install missing plugins if they're added in a detected config change, or if not, maybe there's a way we can have the plugin file send the lua command to trigger it.

The install of the theme can be handled completely from the plugin file as the install line in the config file is actually just fallback to try to install those themes from the Lazy repo.

Again...not fully baked by any stretch and I'm sure something @tahayvr has explored but wanted to get some thoughts and findings out of my head that I think could be helpful here before I forgot them.

ryanrhughes avatar Aug 06 '25 18:08 ryanrhughes

Ok, I take it back! Seems like it is actually using the overrides in the neovim.lua file in themes. You can look at the ristretto theme mini-icons for testing.

The only consideration is that the themes must specify a colorscheme like so:

{
    "LazyVim/LazyVim",
    opts = {
      colorscheme = "bamboo",
    	 },
},

I've added this to the 2 new themes and Catppuccin. We can put a comment in the neovim.lua files that notes this for anyone trying to make themes.

@dhh and @ryanrhughes can you test this solution and let me know if it works on your end?

It's been a bumpy journey but "Per aspera ad astra".

tahayvr avatar Aug 07 '25 18:08 tahayvr

This is super nice, but it doesn't seem entirely bullet proof yet. I tried switching from Kanagawa to Jade and got this weird grey border:

screenshot-2025-08-08_13-41-03

It's supposed to look like this:

screenshot-2025-08-08_13-41-34

I bet because the Kanagawa theme sets something that Jade then doesn't unset. I wonder if there's a way to reset everything between changes?

Also, with the current setup, what's the story on extra themes? Do they just install the theme they need on the next start?

dhh avatar Aug 08 '25 11:08 dhh

Thanks @dhh for testing. This can be fixed by adding a vim.cmd("hi clear") before line 27 where we set the new colorscheme. I'll add this to the pr when I get a chance today.

As for new themes, I need to check how the omarchy-theme-install script is right now after the recent changes. My guess is that I need to add a script (omarchy-theme-neovim-add) that greps the theme's neovim.lua file for the repo name and colorscheme. Then adds it to our omarchy/init.lua file using gawk.

Once it's in the list in omarchy/init.lua, it'll work.

This basically is like a user has installed multiple themes and can switch between them from within neovim because they are already installed, loaded, and available. We are just automating the install/load/switching.

tahayvr avatar Aug 08 '25 17:08 tahayvr

Awesome! Thanks for pursuing this all the way. It is annoying that you have to restart neovim right now to pickup the new theme. This is going to be much nicer 👌

dhh avatar Aug 08 '25 19:08 dhh

I spent some time with this and tried a bit of a different take building on top of this concept to give us some options to look at.

The one thing I really don't like about this current setup is the requirement to maintain sort of a registry of themes in Neovim so I wanted to see what it'd take to eliminate that entirely. I've managed to find a way to remove all of that and just add logic to:

  1. Detect whether the theme we're calling isn't installed, in which case one instance installs while the others wait until that's finished and refresh themselves.
  2. Detect whether the theme is installed, but just not loaded, in which case we load the theme and call the colorscheme set.
  3. Call colorscheme set successfully because the theme is already loaded and ready to go.

In the event the theme is installed and just not called in a plugin, it still sticks around unless you run a Lazy Clean event. After that, it'd have to re-clone it.

https://github.com/basecamp/omarchy/compare/dev...ryanrhughes:omarchy:reload-neovim-themes

Sticking with the registry may be the simpler path in the end given the constraints we have in the current Lazy setup but figured it was worth a good swing for the learnings.

There is also some way we could possibly make that registry just a bunch of symlinks by having something like neovim.lua that has the them plugin definition, and neovim-active.lua that activates that theme specifically. When we install a theme, we just symlink all neovim.lua to ~/.config/nvim/plugins/{theme_name}.lua so they'll all trigger to be installed based on that vs having to maintain an init.lua file with all of them. neovim-active.lua effectively acts like our current one. Happy to flesh out an experiment of this idea if we think it has legs.

Regardless, we'll probably want to pull this bit over as just setting the colorscheme, as @dhh noticed doesn't fully apply our theme since we have an after plugin that runs. We need to re-run that after the theme is updated.

https://github.com/ryanrhughes/omarchy/blob/d81f0894f73b07bfcfa18b9ce7cd684dee8af207/config/nvim/lua/plugins/omarchy-theme.lua#L5-L9

ryanrhughes avatar Aug 09 '25 00:08 ryanrhughes

Added a vim.cmd("hi clear") that clears all the highlight settings before appying the new colorscheme. I don't see the gray gutter that @dhh shows in his screenshots anymore.

Also added a new omarchy-theme-neovim-add script to the theme-install command. I tested it on some of the "extra themes" in the manual.

@ryanrhughes I'm going to look into your suggestions (much appreciated), but curious if you are still having style issues with the new updates to this PR.

tahayvr avatar Aug 09 '25 07:08 tahayvr

I added a function that checks for the light.mode file in theme folders and sets background = light based on that. No need to specify background in the theme files anymore.

@dhh I think we're ready! The only thing to add in the Manual is:

Themes have to specify the colorscheme in their neovim.lua file.

{
    "LazyVim/LazyVim",
    opts = {
      colorscheme = "bamboo",
    	 },
}

tahayvr avatar Aug 12 '25 04:08 tahayvr

@tahayvr this is super close in my opinion. I left a few comments in the code and a few below that I observed. Loving how this is shaping up. SO much nicer to not have to restart Neovim all the time!

Small Artifact Remaining

There's one very small artifact that isn't getting changed I noticed. It's really only noticeable when going from light to dark so like Latte to Matte Black or vice versa. Would be nice to find how to clobber this one too. image

Install.colorscheme

It looks like we still need to remove all of the install.colorscheme logic here. I believe the naming of it has caused some confusion here but the install.colorscheme doesn't have anything to do with installing colorschemes and rather is the initial color scheme it chooses for running the installer -- like when you open Neovim and the installer immediately pops up. It'll always choose the first one it finds so adding them all to a list just gives us more to maintain with no net benefit. If anything, we should be ensuring that the only one in this list is the currently active theme so you don't see a slight flash if the installer comes up when you open Nvim.

I suppose we can keep it if we'd like but it just seems like something that can go wrong that offers no benefit.

This does not impact whether or not the plugin and colorscheme itself is actually loaded and available -- that is handled by the plugin definition.

Ref: https://lazy.folke.io/configuration

  install = {
    -- install missing plugins on startup. This doesn't increase startup time.
    missing = true,
    -- try to load one of these colorschemes when starting an installation during startup
    colorscheme = { "habamax" },
  },

Error on Load

If we load Nvim with a theme that has been installed before this update, we get this error and the theme doesn't load at all. This is a bit odd since it meets the spec but appears that there's something causing it to not load. Reinstalling the theme causes it to work fine. Ref: https://github.com/perfektnacht/omarchy-wasteland-theme

image

Error when Switching

Likely related to the above but if I switched to a theme that was installed before, this error comes up but the theme does seem to apply correctly. We may just need to do some error handling or something. Ref: https://github.com/bjarneo/omarchy-ash-theme

Error applying colorscheme: vim/_editor.lua:0: nvim_exec2()[1]..User Autocommands for "OmarchyThemeReload"..script nvim_exec2() called at User Autocommands for "OmarchyThemeReload":0, line 1: Vim(colorscheme):E185: Cannot find color scheme 'ash'

Parsing Error

In installed the new Azure Glow theme and encountered a few errors. I believe they're driven in part by how that theme is implementing Neovim colorsheme but maybe something we should try to catch if we can or consider how we fallback gracefully in this situation.

Ref: https://github.com/Hydradevx/omarchy-azure-glow-theme/blob/main/neovim.lua

It added this to my init.lua which obviously causes Lazy to be mad when it tries to load. It's also worth noting that the theme does not apply which I find strange since the colorscheme definition in the plugin should be doing the work so this theme should technically still work since it has the color definitions in the colorscheme that should be being set.

image image

Migrating Existing Installed Themes

With the errors above, it raises the question; can we / should we handle some of this via migration? It seems plausible that a lot of people will have installed themes and feels bad to add a feature that doesn't work unless you reinstall all of your themes. Could also be totally fair to say that we only support this out of the box for core themes but if it's easy enough to add a migration to do a onetime parsing of everything that isn't a symlink, I'd be for that.

ryanrhughes avatar Aug 12 '25 20:08 ryanrhughes

We definitely need to do a migration to existing extra themes that have been installed. Or have a way where there's a fallback where it just doesn't live change when that happens.

dhh avatar Aug 23 '25 06:08 dhh

We definitely need to do a migration to existing extra themes that have been installed. Or have a way where there's a fallback where it just doesn't live change when that happens.

I am close to having something ready for the migration. Right now, if a theme is not initialized in our /.config/nvim/lua/omarchy/init.lua, the live change fails and neovim just stays on the last theme.

As for the themes like Azure Glow (mentioned above by @ryanrhughes ) that do not follow our standard configuration for neovim.lua, It simply is not gonna work. I will add a validation step in the omarchy-theme-neovim-add that will skip these and not add a wrong entry in our init.lua file.

tahayvr avatar Aug 23 '25 07:08 tahayvr

When it doesn't work, what happens? Just don't want to brick a system.

dhh avatar Aug 23 '25 09:08 dhh

I added validation to omarchy-theme-sync-nvim (new name) which run as part of omarchy-theme-install and checks the neovim.lua file if it's compatible or not.

  • If it has a repo/name and colorscheme specified it'll add the theme to /.config/nvim/lua/omarchy/init.lua:
2025-08-23-014210_hyprshot
  • If it does not have those values specified (like the Azure Glow theme) it'll skip the sync so live theme loading will not work with that theme. Theme still installs though:
2025-08-23-014634_hyprshot

tahayvr avatar Aug 23 '25 09:08 tahayvr

@dhh I also added a migration script that goes through all the installed themes and adds the missing ones to /.config/nvim/lua/omarchy/init.lua

tahayvr avatar Aug 23 '25 09:08 tahayvr

@dhh I'm gonna test the updated code on a fresh machine when the new ISO install is successful. Curious to know how your tests go.

tahayvr avatar Aug 24 '25 15:08 tahayvr

Fixed the conflict caused by the new file structure.

tahayvr avatar Aug 27 '25 20:08 tahayvr

Why we need this ? I don’t want my Neovim to be bloated, just stick with one colorscheme.

phucisstupid avatar Aug 30 '25 00:08 phucisstupid

There's no meaningful bloat by having some themes installed but not loaded. Totally worth it for fast theme switching.

dhh avatar Aug 30 '25 07:08 dhh

Would it be possible to make a neovim plugin out of this? I prefer to have my own neovim config and I think other people would like this too.

nik-here avatar Sep 01 '25 08:09 nik-here

it's been a while and this thread has become too long, so here's a summary of where we are right now:

  • We add an omarchy folder in our Neovim config folder that contains:
  1. a config/nvim/lua/omarchy/init.lua file that lists the theme & colorscheme names for LazyVim to load.
  2. a config/nvim/lua/omarchy/theme_reload.lua that has the logic for the theme switch. This is where we get the signal for theme changes, recognize the new theme, clear the previous colors, set the light or dark background and finally, set the new colorscheme.
  • We add config/nvim/lua/plugins/omarchy-themes.lua that tells makes this omarchy folder act like a plugin (sort of)
  • We add a line init neovim's main init.lua that tells neovim this plugin exists and required.
 -- Setup theme reloader
require("omarchy.theme_reload").setup()
  • We have a script called bbin/omarchy-theme-sync-nvim that can parse neovim.lua files in theme folders and adds the plugin name and colorscheme to config/nvim/lua/omarchy/init.lua

And finaly:

  • We change the system theme neovim.lua files to matrch our standard format.
  • We have 2 migration files:
  1. Applies the changes above to neovim config folder and theme files.
  2. Goes through the existing themes that are installed and parses for theme names and colorschemes and adds them to the config/nvim/lua/omarchy/init.lua file. Similar to our omarchy-theme-sync-nvim script.

If you have tested this and have issues, let me know so I can investigate.

How omarchy-theme-install output looks now:

2025-09-05-231116_hyprshot

tahayvr avatar Sep 06 '25 05:09 tahayvr

I really appreciate how far you've pushed on this, Taha, but this is an enormous amount of code. This shouldn't be this hard. The reason it is is that we're trying to make all these code-dumps that just happen to change colors work. I think we need to go back to the drawing board. Come up with a unified format for changing nvim colors, then build a single plugin that can take this specification, live reload changes, and take the single color definition as an input.

dhh avatar Sep 15 '25 15:09 dhh