cwal is a fast and lightweight command-line tool for generating dynamic color schemes from images. It extracts dominant colors from your chosen image and applies them to your terminal, applications, and other system components, providing a cohesive and visually appealing desktop experience.
- Dynamic Color Generation: Extracts a vibrant 16-color palette from any image
- Advanced Backend Support: Utilizes
imagemagickorlibimagequantfor efficient color quantization - Lua Scripting Support: Create custom backends using Lua scripts for advanced color quantization
- Extensive Customization: Fine-tune saturation, contrast, alpha transparency, and theme mode (dark/light)
- Template-Based Output: Generates color schemes for various applications using customizable templates
- Automatic Application Reloading: Seamlessly integrates with your system to apply changes instantly
- Palette Preview: View the generated color palette directly in your terminal
- Random Image Selection: Automatically pick a random image from any specified directory
- Theme Management: Load predefined themes or select random themes based on mode (dark/light/all)
- Dark mode
- Light mode
cwal requires imagemagick, libimagequant, and lua as dependencies.
Ensure the following libraries are installed on your system:
imagemagicklibimagequantlua(orliblua-dev)
Ubuntu/Debian:
sudo apt install imagemagick libimagequant-dev liblua5.4-devArch Linux:
sudo pacman -S imagemagick libimagequant luaFedora/RHEL:
sudo dnf install ImageMagick-devel libimagequant-devel lua-develmacOS
brew install imagemagick libimagequant luaInstall directly from the AUR:
yay -S cwal
# or
paru -S cwal- Clone the repository:
git clone https://github.com/nitinbhat972/cwal.git
cd cwal- Build and install:
User-specific:
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local ..
make
make installSystem-wide:
mkdir build && cd build
cmake ..
make
sudo make installUsage: cwal [OPTIONS] --img <image_path>--img <image_path>Specify the image path (required)--mode <dark|light>Set theme mode--cols16-mode <darken|lighten>Set 16-color mode--saturation <float>Overall saturation--contrast <float>Contrast ratio--alpha <float>Alpha transparency (0.0-1.0)--out-dir <path>Output directory for generated files--backend <name>Set image processing backend--script <script_path>Run custom script after processing--no-reloadDisable reloading--list-backendsList available backends--list-themesList all available themes--quietSuppress all output--random <directory>Select random image from directory--theme <theme_name|random_dark|random_light|random_all>Select a theme or a random one--previewPreview palette--helpHelp
Examples:
cwal --img /path/to/image.jpg
cwal --img /path/to/image.png --mode dark --saturation 0.2
cwal --img /path/to/image.jpg --preview
cwal --random ~/Pictures/wallpapers
cwal --theme random_dark
cwal --theme random_light
cwal --theme random_all
cwal --list-themes
cwal --img /path/to/image.jpg --out-dir ~/.config/colors --script ~/.local/bin/reload-apps.shTemplates are stored in:
/usr/local/share/cwal/templates(system-wide)~/.config/cwal/templates(user)
Supported apps: Terminal emulators (Alacritty, Kitty, Wezterm), window managers (i3, bspwm, Hyprland), text editors (Vim, Neovim, VS Code), system themes (GTK, Qt).
cwal templates support various color formatting options. You can use these formats within your templates to customize the output for different applications.
| Format Specifier | Description | Example Output (for color with R=255, G=128, B=0, Alpha=0.8) |
|---|---|---|
hex |
Hexadecimal color code (e.g., #RRGGBB) |
#ff8000 |
xhex |
Hexadecimal color code with 0x prefix |
0xff8000 |
strip |
Hexadecimal color code without prefix | ff8000 |
rgb |
RGB format (e.g., rgb(R,G,B)) |
rgb(255,128,0) |
rgba |
RGBA format (e.g., rgba(R,G,B,A)) |
rgba(255,128,0,0.8) |
red |
Red component value (0-255) | 255 |
green |
Green component value (0-255) | 128 |
blue |
Blue component value (0-255) | 0 |
alpha_dec |
Alpha transparency value (0.0-1.0) | 0.8 |
Example usage in a template:
# For color0 (background)
background = {color0.hex}
background_rgb = {color0.rgb}
background_alpha = {color0.rgba}
# For color1 (foreground)
foreground = {color1.strip}
foreground_red = {color1.red}
- Check available backends:
cwal --list-backends - Choose backend:
cwal --img image.jpg --backend libimagequant - Post-process:
cwal --img image.jpg --script ~/.local/bin/update-theme.sh - Batch processing:
for img in ~/Pictures/wallpapers/*.{jpg,png,jpeg}; do
cwal --img "$img" --quiet
donecwal now supports custom backends using Lua scripts. This allows you to implement your own color quantization algorithms or image processing techniques.
To create a custom backend:
-
Create a Lua script with a
Main(image_path)function that returns a table of 16 colors, each as{r, g, b}wherer,g,bare integers 0-255. -
Place the script in
~/.config/cwal/backends/(the directory will be created if it doesn't exist). -
Use the backend by its name (script filename without
.lua) with--backend <name>.
The script receives the image path and should process it to generate the palette.
Example
local ffi = require("ffi")
local function raw_to_pixels(data, size)
local expected = size * size * 3
if #data < expected then
return nil, string.format("expected >= %d bytes, got %d", expected, #data)
end
local buf = ffi.cast("const unsigned char*", data)
local pixels = {}
pixels[#pixels + size * size] = false -- preallocate
local idx = 1
for i = 0, size * size - 1 do
local base = i * 3
pixels[idx] = { buf[base], buf[base + 1], buf[base + 2] }
idx = idx + 1
end
return pixels
end
local function try_read_pixels_with(path, size)
local quoted = string.format('"%s"', path)
local conv = string.format("magick %s -resize %dx%d! -colorspace sRGB -depth 8 rgb:-", quoted, size, size)
local f = io.popen(conv, "r")
if not f then
return nil, "popen failed"
end
local data = f:read("*all")
f:close()
if not data or #data == 0 then
return nil, "no data"
end
return raw_to_pixels(data, size)
end
local function read_pixels(path, size)
local px, err = try_read_pixels_with(path, size)
if px then
return px
end
error("Could not read pixels via ImageMagick: " .. tostring(err))
end
local function dist2(a, b)
local dr = a[1] - b[1]
local dg = a[2] - b[2]
local db = a[3] - b[3]
return dr * dr + dg * dg + db * db
end
local function init_centroids_kpp(pixels, k)
local n = #pixels
if k > n then
k = n
end
local centroids = {}
local i1 = math.random(n)
centroids[1] = { pixels[i1][1], pixels[i1][2], pixels[i1][3] }
local function nearest_d2(p)
local best = math.huge
for i = 1, #centroids do
local d = dist2(p, centroids[i])
if d < best then
best = d
end
end
return best
end
while #centroids < k do
local dsum, d2s = 0.0, {}
for i = 1, n do
local d = nearest_d2(pixels[i])
d2s[i] = d
dsum = dsum + d
end
local r, acc = math.random() * dsum, 0.0
for i = 1, n do
acc = acc + d2s[i]
if acc >= r then
local p = pixels[i]
centroids[#centroids + 1] = { p[1], p[2], p[3] }
break
end
end
if #centroids < 2 then
break
end
end
return centroids
end
local function kmeans(pixels, k, max_iter)
max_iter = max_iter or 25
local n = #pixels
if n == 0 then
return {}
end
if k < 1 then
k = 1
elseif k > n then
k = n
end
math.randomseed(tonumber(tostring(os.clock()):gsub("%D", "")))
local centroids = init_centroids_kpp(pixels, k)
local assign = ffi.new("int[?]", n)
local changed, iter = true, 0
while changed and iter < max_iter do
iter, changed = iter + 1, false
-- assign step
for i = 1, n do
local p = pixels[i]
local best_k, best_d = 1, dist2(p, centroids[1])
for c = 2, k do
local d = dist2(p, centroids[c])
if d < best_d then
best_d, best_k = d, c
end
end
if assign[i - 1] ~= best_k then
assign[i - 1] = best_k
changed = true
end
end
if not changed then
break
end
-- update step
local sumR, sumG, sumB, count = {}, {}, {}, {}
for c = 1, k do
sumR[c], sumG[c], sumB[c], count[c] = 0, 0, 0, 0
end
for i = 1, n do
local c = assign[i - 1]
local p = pixels[i]
sumR[c] = sumR[c] + p[1]
sumG[c] = sumG[c] + p[2]
sumB[c] = sumB[c] + p[3]
count[c] = count[c] + 1
end
for c = 1, k do
if count[c] > 0 then
centroids[c][1] = sumR[c] / count[c]
centroids[c][2] = sumG[c] / count[c]
centroids[c][3] = sumB[c] / count[c]
else
local rp = pixels[math.random(n)]
centroids[c][1], centroids[c][2], centroids[c][3] = rp[1], rp[2], rp[3]
changed = true
end
end
end
-- build palette
local palette = {}
for c = 1, k do
local r = centroids[c][1] + 0.5
if r < 0 then
r = 0
elseif r > 255 then
r = 255
end
local g = centroids[c][2] + 0.5
if g < 0 then
g = 0
elseif g > 255 then
g = 255
end
local b = centroids[c][3] + 0.5
if b < 0 then
b = 0
elseif b > 255 then
b = 255
end
palette[#palette + 1] = {
math.floor(r),
math.floor(g),
math.floor(b),
}
end
return palette
end
local function sort_by_population(pixels, palette)
local k, counts = #palette, {}
for c = 1, k do
counts[c] = 0
end
for i = 1, #pixels do
local p, best_c, best_d = pixels[i], 1, dist2(p, palette[1])
for c = 2, k do
local d = dist2(p, palette[c])
if d < best_d then
best_d, best_c = d, c
end
end
counts[best_c] = counts[best_c] + 1
end
local idx = {}
for c = 1, k do
idx[c] = c
end
table.sort(idx, function(a, b)
return counts[a] > counts[b]
end)
local sorted = {}
for i = 1, k do
sorted[i] = palette[idx[i]]
end
return sorted
end
function Main(image_path)
local k, sample_size, max_iter = 16, 128, 25
local pixels = read_pixels(image_path, sample_size)
local palette = kmeans(pixels, k, max_iter)
palette = sort_by_population(pixels, palette)
while #palette > k do
table.remove(palette)
end
while #palette < k do
local last = palette[#palette]
palette[#palette + 1] = { last[1], last[2], last[3] }
end
return palette
endReport issues, request features, or contribute via PRs. See the GitHub repository for more info.
Licensed under GNU GPL v3.0 β always free and open-source.
Star the project on GitHub if you find it useful!
-
pywal by dylanaraps












