.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Base46: Pywal Edition
2 |
3 | Pywal and Matugen support for NvChad!
4 |
5 | > [!NOTE]
6 | > Support for Pywal requires these Python libraries to be installed:
7 | > - `pywal`
8 | > - `watchdog`
9 |
10 | ## Installation
11 | ```bash
12 | cd ~/.config/nvim
13 | git clone https://github.com/NvChad/pywal
14 | ```
15 | Add this at the end of your `init.lua` file:
16 | ```lua
17 | os.execute("python ~/.config/nvim/pywal/chadwal.py &> /dev/null &")
18 |
19 | local autocmd = vim.api.nvim_create_autocmd
20 |
21 | autocmd("Signal", {
22 | pattern = "SIGUSR1",
23 | callback = function()
24 | require('nvchad.utils').reload()
25 | end
26 | })
27 | ```
28 | Now you need to generate your Pywal theme again using `wal -i `. If not, `chadwal` will default to `gruvchad` colors.
29 |
30 | ### Matugen support
31 | Add this line to the `[templates]` section of your `~/.config/matugen/config.toml` file:
32 | ```toml
33 | nvim = { input_path = '~/.config/nvim/pywal/matugen.lua', output_path = '~/.cache/wal/base46-dark.lua' }
34 | ```
35 |
36 | Alternative syntax:
37 | ```toml
38 | [templates.nvim]
39 | input_path = '~/.config/nvim/pywal/matugen.lua'
40 | output_path = '~/.cache/wal/base46-dark.lua'
41 | ```
42 |
43 | Just like with Pywal, you need to generate your theme again using `matugen image `. If not, `chadwal` will default to `gruvchad` colors.
44 |
45 | Select `chadwal` theme and enjoy!
46 |
47 | ## Demo
48 | https://github.com/user-attachments/assets/933c97f2-4566-406c-8c04-e2e9f0f3f6da
49 |
--------------------------------------------------------------------------------
/chadwal.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | import shutil
5 | import sys
6 | import time
7 | import subprocess
8 | from watchdog.observers import Observer
9 | from watchdog.events import FileSystemEventHandler
10 |
11 | # Constants
12 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
13 | HOME_DIR = os.path.expanduser("~")
14 | TEMPLATE_SRC = {
15 | "dark": os.path.join(SCRIPT_DIR, "dark.lua"),
16 | "light": os.path.join(SCRIPT_DIR, "light.lua")
17 | }
18 | TEMPLATE_DST = {
19 | "dark": f"{HOME_DIR}/.config/wal/templates/base46-dark.lua",
20 | "light": f"{HOME_DIR}/.config/wal/templates/base46-light.lua"
21 | }
22 | CACHE_SRC = {
23 | "dark": f"{HOME_DIR}/.cache/wal/base46-dark.lua",
24 | "light": f"{HOME_DIR}/.cache/wal/base46-light.lua"
25 | }
26 | CACHE_DST = f"{HOME_DIR}/.local/share/nvim/lazy/base46/lua/base46/themes/chadwal.lua"
27 | FALLBACK_THEME = f"{HOME_DIR}/.local/share/nvim/lazy/base46/lua/base46/themes/gruvchad.lua"
28 | LOCK_FILE = "/tmp/wal_nvim_lock"
29 | COLORS_FILE = f"{HOME_DIR}/.cache/wal/colors"
30 |
31 | # Utility functions
32 | def is_dark(hex_color):
33 | """Determine if the color is dark based on luminance."""
34 | hex_color = hex_color.lstrip('#')
35 | r, g, b = (int(hex_color[i:i+2], 16) for i in (0, 2, 4))
36 | brightness = (r * 299 + g * 587 + b * 114) / 1000
37 | return brightness < 128
38 |
39 | def get_hex_from_colors_file():
40 | """Read the first color from the colors file."""
41 | try:
42 | with open(COLORS_FILE, 'r') as file:
43 | return file.readline().strip()
44 | except FileNotFoundError:
45 | sys.exit(f"Error: Colors file not found: {COLORS_FILE}")
46 |
47 | def acquire_lock():
48 | """Create a lock file to prevent multiple instances."""
49 | if os.path.exists(LOCK_FILE):
50 | sys.exit("Another instance is already running. Exiting...")
51 | open(LOCK_FILE, 'w').close()
52 |
53 | def release_lock():
54 | """Remove the lock file."""
55 | if os.path.exists(LOCK_FILE):
56 | os.remove(LOCK_FILE)
57 |
58 | def copy_file(src, dst, skip_if_exists=False):
59 | """Copy a file from src to dst, optionally skipping if dst exists."""
60 | if skip_if_exists and os.path.exists(dst):
61 | print(f"File already exists at {dst}, skipping copy.")
62 | return
63 |
64 | try:
65 | os.makedirs(os.path.dirname(dst), exist_ok=True)
66 | shutil.copy(src, dst)
67 | print(f"File copied from {src} to {dst}.")
68 | except Exception as e:
69 | sys.exit(f"Error copying file from {src} to {dst}: {e}")
70 |
71 | def on_file_modified():
72 | """Handle file modifications based on current color scheme."""
73 | is_dark_theme = is_dark(get_hex_from_colors_file())
74 | mode = "dark" if is_dark_theme else "light"
75 |
76 | copy_file(FALLBACK_THEME, CACHE_SRC[mode], skip_if_exists=True)
77 | copy_file(TEMPLATE_SRC[mode], TEMPLATE_DST[mode])
78 | copy_file(CACHE_SRC[mode], CACHE_DST)
79 |
80 | # Signal running nvim instances, don't exit based on the result here.
81 | subprocess.run(['killall', '-SIGUSR1', 'nvim'])
82 |
83 | # Watchdog event handler
84 | class MyHandler(FileSystemEventHandler):
85 | def on_modified(self, event):
86 | if event.src_path == CACHE_SRC["dark"]:
87 | on_file_modified()
88 |
89 | def monitor_file(file_path):
90 | """Monitor the specified file for changes."""
91 | event_handler = MyHandler()
92 | observer = Observer()
93 | observer.schedule(event_handler, os.path.dirname(file_path), recursive=False)
94 | observer.start()
95 |
96 | try:
97 | while True:
98 | # Check every 5 seconds if nvim is running
99 | time.sleep(5)
100 | # Use pgrep to check for exact process name 'nvim'
101 | result = subprocess.run(['pgrep', '-x', 'nvim'], capture_output=True)
102 | if result.returncode != 0:
103 | print("No running nvim instances found. Exiting.")
104 | observer.stop() # Stop the observer thread
105 | break # Exit the loop
106 | except KeyboardInterrupt:
107 | print("\nKeyboard interrupt received. Exiting.")
108 | observer.stop()
109 | finally:
110 | # Ensure the observer thread is joined before exiting
111 | observer.join()
112 |
113 | if __name__ == "__main__":
114 | on_file_modified()
115 | acquire_lock()
116 | try:
117 | on_file_modified()
118 | monitor_file(CACHE_SRC["dark"])
119 | finally:
120 | release_lock()
121 |
--------------------------------------------------------------------------------
/dark.lua:
--------------------------------------------------------------------------------
1 | local M = {{}}
2 |
3 | local lighten = require("base46.colors").change_hex_lightness
4 |
5 | M.base_30 = {{
6 | white = "{color7}",
7 | darker_black = lighten("{color0}", -3),
8 | black = "{color0}",
9 | black2 = lighten("{color0}", 6),
10 | one_bg = lighten("{color0}", 10),
11 | grey = lighten("{color0}", 40),
12 | light_grey = "{color8}",
13 | red = "{color1}",
14 | baby_pink = "{color9}",
15 | pink = "{color13}",
16 | line = "{color8}",
17 | green = "{color2}",
18 | vibrant_green = "{color2}",
19 | nord_blue = "{color4}",
20 | blue = "{color4}",
21 | yellow = "{color3}",
22 | sun = lighten("{color3}", 6),
23 | purple = "{color13}",
24 | dark_purple = "{color13}",
25 | teal = "{color4}",
26 | orange = "{color1}",
27 | cyan = "{color4}",
28 | pmenu_bg = "{color8}",
29 | folder_bg = "{color4}",
30 | }}
31 |
32 | M.base_30.statusline_bg = M.base_30.black2
33 | M.base_30.lightbg = M.base_30.one_bg
34 | M.base_30.one_bg2 = lighten(M.base_30.one_bg, 6)
35 | M.base_30.one_bg3 = lighten(M.base_30.one_bg2, 6)
36 | M.base_30.grey_fg = lighten(M.base_30.grey, 10)
37 | M.base_30.grey_fg2 = lighten(M.base_30.grey, 5)
38 |
39 | M.base_16 = {{
40 | base00 = "{color0}",
41 | base01 = "{color0}",
42 | base02 = "{color8}",
43 | base03 = "{color8}",
44 | base04 = "{color7}",
45 | base05 = "{color7}",
46 | base06 = "{color15}",
47 | base07 = "{color15}",
48 | base08 = "{color1}",
49 | base09 = "{color2}",
50 | base0A = "{color3}",
51 | base0B = "{color4}",
52 | base0C = "{color5}",
53 | base0D = "{color6}",
54 | base0E = "{color1}",
55 | base0F = "{color15}",
56 | }}
57 |
58 | M.type = "dark"
59 |
60 | M.polish_hl = {{
61 | Operator = {{
62 | fg = M.base_30.nord_blue,
63 | }},
64 |
65 | ["@operator"] = {{
66 | fg = M.base_30.nord_blue,
67 | }},
68 | }}
69 |
70 | return M
71 |
--------------------------------------------------------------------------------
/light.lua:
--------------------------------------------------------------------------------
1 | local M = {{}}
2 |
3 | local lighten = require("base46.colors").change_hex_lightness
4 |
5 | M.base_30 = {{
6 | white = "{color7}",
7 | darker_black = lighten("{color0}", -3),
8 | black = "{color0}",
9 | black2 = lighten("{color0}", -6),
10 | one_bg = lighten("{color0}", -10),
11 | grey = lighten("{color0}", -40),
12 | light_grey = "{color8}",
13 | red = "{color1}",
14 | baby_pink = "{color9}",
15 | pink = "{color13}",
16 | line = "{color8}",
17 | green = "{color2}",
18 | vibrant_green = "{color2}",
19 | nord_blue = "{color4}",
20 | blue = "{color4}",
21 | yellow = "{color3}",
22 | sun = lighten("{color3}", -6),
23 | purple = "{color13}",
24 | dark_purple = "{color13}",
25 | teal = "{color4}",
26 | orange = "{color1}",
27 | cyan = "{color4}",
28 | pmenu_bg = "{color8}",
29 | folder_bg = "{color4}",
30 | }}
31 |
32 | M.base_30.statusline_bg = M.base_30.black2
33 | M.base_30.lightbg = M.base_30.one_bg
34 | M.base_30.one_bg2 = lighten(M.base_30.one_bg, -6)
35 | M.base_30.one_bg3 = lighten(M.base_30.one_bg2, -6)
36 | M.base_30.grey_fg = lighten(M.base_30.grey, -10)
37 | M.base_30.grey_fg2 = lighten(M.base_30.grey, -5)
38 |
39 | M.base_16 = {{
40 | base00 = "{color0}",
41 | base01 = "{color0}",
42 | base02 = "{color8}",
43 | base03 = "{color8}",
44 | base04 = "{color7}",
45 | base05 = "{color7}",
46 | base06 = "{color15}",
47 | base07 = "{color15}",
48 | base08 = "{color1}",
49 | base09 = "{color2}",
50 | base0A = "{color3}",
51 | base0B = "{color4}",
52 | base0C = "{color5}",
53 | base0D = "{color6}",
54 | base0E = "{color1}",
55 | base0F = "{color15}",
56 | }}
57 |
58 | M.type = "dark"
59 |
60 | M.polish_hl = {{
61 | Operator = {{
62 | fg = M.base_30.nord_blue,
63 | }},
64 |
65 | ["@operator"] = {{
66 | fg = M.base_30.nord_blue,
67 | }},
68 | }}
69 |
70 | return M
71 |
--------------------------------------------------------------------------------
/matugen.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | local lighten = require("base46.colors").change_hex_lightness
4 |
5 | M.base_30 = {
6 | white = '{{colors.on_background.default.hex}}',
7 | black = '{{colors.background.default.hex}}',
8 | darker_black = lighten('{{colors.background.default.hex}}', -3),
9 | black2 = lighten('{{colors.background.default.hex}}', 6),
10 | one_bg = lighten('{{colors.background.default.hex}}', 10),
11 | one_bg2 = lighten('{{colors.background.default.hex}}', 16),
12 | one_bg3 = lighten('{{colors.background.default.hex}}', 22),
13 | grey = '{{colors.surface_variant.default.hex}}',
14 | grey_fg = lighten('{{colors.surface_variant.default.hex}}', -10),
15 | grey_fg2 = lighten('{{colors.surface_variant.default.hex}}', -20),
16 | light_grey = '{{colors.outline.default.hex}}',
17 | red = '{{colors.error.default.hex}}',
18 | baby_pink = lighten('{{colors.error.default.hex}}', 10),
19 | pink = '{{colors.tertiary.default.hex}}',
20 | line = '{{colors.outline.default.hex}}',
21 | green = '{{colors.secondary.default.hex}}',
22 | vibrant_green = lighten('{{colors.secondary.default.hex}}', 10),
23 | blue = '{{colors.primary.default.hex}}',
24 | nord_blue = lighten('{{colors.primary.default.hex}}', 10),
25 | yellow = lighten('{{colors.tertiary.default.hex}}', 10),
26 | sun = lighten('{{colors.tertiary.default.hex}}', 20),
27 | purple = '{{colors.tertiary.default.hex}}',
28 | dark_purple = lighten('{{colors.tertiary.default.hex}}', -10),
29 | teal = '{{colors.secondary_container.default.hex}}',
30 | orange = '{{colors.error.default.hex}}',
31 | cyan = '{{colors.secondary.default.hex}}',
32 | statusline_bg = lighten('{{colors.background.default.hex}}', 6),
33 | pmenu_bg = '{{colors.surface_variant.default.hex}}',
34 | folder_bg = lighten('{{colors.primary_fixed_dim.default.hex}}', 0),
35 | lightbg = lighten('{{colors.background.default.hex}}', 10),
36 | }
37 |
38 | M.base_16 = {
39 | base00 = '{{colors.surface.default.hex}}',
40 | base01 = lighten('{{colors.surface_variant.default.hex}}', 0),
41 | base02 = '{{colors.secondary_fixed_dim.default.hex}}',
42 | base03 = lighten('{{colors.outline.default.hex}}', 0),
43 | base04 = lighten('{{colors.on_surface_variant.default.hex}}', 0),
44 | base05 = '{{colors.on_surface.default.hex}}',
45 | base06 = lighten('{{colors.on_surface.default.hex}}', 0),
46 | base07 = '{{colors.surface.default.hex}}',
47 | base08 = lighten('{{colors.error.default.hex}}', -10),
48 | base09 = '{{colors.tertiary.default.hex}}',
49 | base0A = '{{colors.primary.default.hex}}',
50 | base0B = '{{colors.tertiary_fixed.default.hex}}',
51 | base0C = '{{colors.primary_fixed_dim.default.hex}}',
52 | base0D = lighten('{{colors.primary_container.default.hex}}', 20),
53 | base0E = '{{colors.on_primary_container.default.hex}}',
54 | base0F = '{{colors.inverse_surface.default.hex}}',
55 | }
56 |
57 | M.type = "dark" -- or "light" depending on your theme
58 |
59 | M.polish_hl = {
60 | defaults = {
61 | Comment = {
62 | italic = true,
63 | fg = M.base_16.base03,
64 | },
65 | },
66 | Syntax = {
67 | String = {
68 | fg = '{{colors.tertiary.default.hex}}'
69 | }
70 | },
71 | treesitter = {
72 | ["@comment"] = {
73 | fg = M.base_16.base03,
74 | },
75 | ["@string"] = {
76 | fg = '{{colors.tertiary.default.hex}}'
77 | },
78 | }
79 | }
80 |
81 | return M
82 |
--------------------------------------------------------------------------------