mirror of
https://github.com/danth/stylix
synced 2024-11-10 06:34:15 +00:00
Rewrite palette generation in Haskell ✨
This does not rely on an external library for colour selection, therefore it can be fine-tuned to create a better theme. Closes #2 because Colorgram is no longer used.
This commit is contained in:
parent
ed67f81454
commit
7b34be82ff
9 changed files with 297 additions and 190 deletions
18
default.nix
18
default.nix
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
imports = [
|
||||
./stylix/default.nix
|
||||
|
||||
./modules/console.nix
|
||||
./modules/dunst.nix
|
||||
./modules/feh.nix
|
||||
./modules/fish.nix
|
||||
./modules/grub.nix
|
||||
./modules/gtk.nix
|
||||
./modules/kitty.nix
|
||||
./modules/lightdm.nix
|
||||
./modules/plymouth
|
||||
./modules/qutebrowser.nix
|
||||
./modules/sway.nix
|
||||
./modules/vim.nix
|
||||
];
|
||||
}
|
43
flake.lock
Normal file
43
flake.lock
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1633422745,
|
||||
"narHash": "sha256-gA6Ok64nPbkjHk3Oanq4641EeYkjcKhisDF9wBjLxEk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8e1eab9eae4278c9bb1dcae426848a581943db5a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"utils": "utils"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1631561581,
|
||||
"narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
56
flake.nix
56
flake.nix
|
@ -1 +1,55 @@
|
|||
{ outputs = inputs: { nixosModules.stylix = import ./default.nix; }; }
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, utils, self, ... }:
|
||||
(utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
ghc = pkgs.haskell.packages.ghc901.ghcWithPackages
|
||||
(haskellPackages: with haskellPackages; [ json JuicyPixels ]);
|
||||
|
||||
palette-generator = pkgs.stdenvNoCC.mkDerivation {
|
||||
name = "palette-generator";
|
||||
src = ./palette-generator;
|
||||
buildInputs = [ ghc ];
|
||||
buildPhase = "ghc -O -threaded -Wall Main.hs";
|
||||
installPhase = "install -D Main $out/bin/palette-generator";
|
||||
};
|
||||
|
||||
palette-generator-app = utils.lib.mkApp {
|
||||
drv = palette-generator;
|
||||
name = "palette-generator";
|
||||
};
|
||||
|
||||
in {
|
||||
packages.palette-generator = palette-generator;
|
||||
apps.palette-generator = palette-generator-app;
|
||||
})) // {
|
||||
nixosModules.stylix = { pkgs, ... }: {
|
||||
imports = [
|
||||
./modules/console.nix
|
||||
./modules/dunst.nix
|
||||
./modules/feh.nix
|
||||
./modules/fish.nix
|
||||
./modules/grub.nix
|
||||
./modules/gtk.nix
|
||||
./modules/kitty.nix
|
||||
./modules/lightdm.nix
|
||||
./modules/plymouth
|
||||
./modules/qutebrowser.nix
|
||||
./modules/sway.nix
|
||||
./modules/vim.nix
|
||||
(import ./stylix/palette.nix
|
||||
self.packages.${pkgs.system}.palette-generator)
|
||||
./stylix/base16.nix
|
||||
./stylix/fonts.nix
|
||||
./stylix/home-manager.nix
|
||||
./stylix/pixel.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
100
palette-generator/Main.hs
Normal file
100
palette-generator/Main.hs
Normal file
|
@ -0,0 +1,100 @@
|
|||
import Codec.Picture ( DynamicImage, Image(imageWidth, imageHeight), PixelRGB8(PixelRGB8), convertRGB8, pixelAt, readImage )
|
||||
import Data.Bifunctor ( second )
|
||||
import Data.List ( sortOn )
|
||||
import Data.Word ( Word8 )
|
||||
import RGBHSV ( HSV(HSV), RGB(RGB), hsvToRgb, rgbToHsv )
|
||||
import System.Environment ( getArgs )
|
||||
import System.Exit ( die )
|
||||
import Text.JSON ( JSObject, encode, toJSObject )
|
||||
import Text.Printf ( printf )
|
||||
|
||||
type OutputTable = JSObject String
|
||||
|
||||
makeOutputTable :: [(String, RGB Float)] -> OutputTable
|
||||
makeOutputTable = toJSObject . concatMap makeOutputs
|
||||
|
||||
where makeOutputs :: (String, RGB Float) -> [(String, String)]
|
||||
makeOutputs (name, RGB r g b) =
|
||||
[ (name ++ "-dec-r", show $ r / 255)
|
||||
, (name ++ "-dec-g", show $ g / 255)
|
||||
, (name ++ "-dec-b", show $ b / 255)
|
||||
, (name ++ "-rgb-r", show r')
|
||||
, (name ++ "-rgb-g", show g')
|
||||
, (name ++ "-rgb-b", show b')
|
||||
, (name ++ "-hex-r", printf "%02x" r')
|
||||
, (name ++ "-hex-g", printf "%02x" g')
|
||||
, (name ++ "-hex-b", printf "%02x" b')
|
||||
, (name ++ "-hex", printf "%02x%02x%02x" r' g' b')
|
||||
, (name ++ "-hash", printf "#%02x%02x%02x" r' g' b')
|
||||
]
|
||||
where r' :: Word8
|
||||
r' = round r
|
||||
g' :: Word8
|
||||
g' = round g
|
||||
b' :: Word8
|
||||
b' = round b
|
||||
|
||||
selectColours :: [HSV Float] -> [(String, HSV Float)]
|
||||
selectColours image = zip names palette
|
||||
|
||||
where names :: [String]
|
||||
names = map (printf "base%02X") ([0..15] :: [Int])
|
||||
|
||||
hueInRange :: (Ord a) => a -> a -> HSV a -> Bool
|
||||
hueInRange low high (HSV hue _ _) = hue >= low && hue < high
|
||||
|
||||
binThresholds :: [Float]
|
||||
binThresholds = [i * (6/9) | i <- [1..9]]
|
||||
|
||||
average :: (Fractional a) => [a] -> a
|
||||
average xs = sum xs / fromIntegral (length xs)
|
||||
|
||||
averageColour :: (Fractional a) => [HSV a] -> HSV a
|
||||
averageColour colours = HSV (average $ map (\(HSV h _ _) -> h) colours)
|
||||
(average $ map (\(HSV _ s _) -> s) colours)
|
||||
(average $ map (\(HSV _ _ v) -> v) colours)
|
||||
|
||||
bins :: [HSV Float]
|
||||
bins = map averageColour
|
||||
$ sortOn length
|
||||
$ map (\bin -> filter (hueInRange (bin - (6/9)) bin) image)
|
||||
binThresholds
|
||||
|
||||
primaryScale :: [HSV Float]
|
||||
primaryScale = [HSV h s (v / 8) | v <- [1..8]]
|
||||
where (HSV h s _) = head bins
|
||||
|
||||
palette :: [HSV Float]
|
||||
palette = primaryScale ++ tail bins
|
||||
|
||||
unpackImage :: DynamicImage -> [RGB Float]
|
||||
unpackImage image = do
|
||||
let image' = convertRGB8 image
|
||||
x <- [0 .. imageWidth image' - 1]
|
||||
y <- [0 .. imageHeight image' - 1]
|
||||
let (PixelRGB8 r g b) = pixelAt image' x y
|
||||
return (RGB (fromIntegral r) (fromIntegral g) (fromIntegral b))
|
||||
|
||||
loadImage :: String -> IO DynamicImage
|
||||
loadImage input = either error id <$> readImage input
|
||||
|
||||
main :: IO ()
|
||||
main = either die mainProcess . parseArguments =<< getArgs
|
||||
|
||||
where parseArguments :: [String] -> Either String (String, String)
|
||||
parseArguments [input, output] = Right (input, output)
|
||||
parseArguments [_] = Left "Please specify an output file"
|
||||
parseArguments [] = Left "Please specify an image"
|
||||
parseArguments _ = Left "Too many arguments"
|
||||
|
||||
mainProcess :: (String, String) -> IO ()
|
||||
mainProcess (input, output) = do
|
||||
putStrLn $ "Processing " ++ input
|
||||
image <- loadImage input
|
||||
let outputTable = makeOutputTable
|
||||
$ map (second hsvToRgb)
|
||||
$ selectColours
|
||||
$ map rgbToHsv
|
||||
$ unpackImage image
|
||||
writeFile output $ encode outputTable
|
||||
putStrLn $ "Saved to " ++ output
|
44
palette-generator/RGBHSV.hs
Normal file
44
palette-generator/RGBHSV.hs
Normal file
|
@ -0,0 +1,44 @@
|
|||
module RGBHSV ( RGB(..), HSV(..), rgbToHsv, hsvToRgb ) where
|
||||
|
||||
import Data.Fixed ( mod' )
|
||||
|
||||
-- http://mattlockyer.github.io/iat455/documents/rgb-hsv.pdf
|
||||
|
||||
data RGB a = RGB a a a deriving (Eq, Show) -- 0 to 255
|
||||
data HSV a = HSV a a a deriving (Eq, Show) -- 0 to 1
|
||||
|
||||
normaliseHue :: (Real a) => a -> a
|
||||
normaliseHue h = h `mod'` 6
|
||||
|
||||
rgbToHsv :: (Eq a, Fractional a, Num a, Real a) => RGB a -> HSV a
|
||||
rgbToHsv (RGB r' g' b') = HSV h' s v
|
||||
where r = r' / 255
|
||||
g = g' / 255
|
||||
b = b' / 255
|
||||
maximal = maximum [r, g, b]
|
||||
minimal = minimum [r, g, b]
|
||||
delta = maximal - minimal
|
||||
h | delta == 0 = 0
|
||||
| maximal == r = (g - b) / delta
|
||||
| maximal == g = ((b - r) / delta) + 2
|
||||
| otherwise = ((r - g) / delta) + 4
|
||||
h' = normaliseHue h
|
||||
s | v == 0 = 0
|
||||
| otherwise = delta / v
|
||||
v = maximal
|
||||
|
||||
hsvToRgb :: (Num a, Ord a, Real a) => HSV a -> RGB a
|
||||
hsvToRgb (HSV h' s v) = RGB r' g' b'
|
||||
where h = normaliseHue h'
|
||||
alpha = v * (1 - s)
|
||||
beta = v * (1 - (h - abs h) * s)
|
||||
gamma = v * (1 - (1 - (h - abs h)) * s)
|
||||
(r, g, b) | h < 1 = (v, gamma, alpha)
|
||||
| h < 2 = (beta, v, alpha)
|
||||
| h < 3 = (alpha, v, gamma)
|
||||
| h < 4 = (alpha, beta, v)
|
||||
| h < 5 = (gamma, alpha, v)
|
||||
| otherwise = (v, alpha, beta)
|
||||
r' = r * 255
|
||||
g' = g * 255
|
||||
b' = b * 255
|
|
@ -1,58 +0,0 @@
|
|||
{ pkgs, lib, config, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.stylix;
|
||||
|
||||
# TODO: This Python library should be included in Nixpkgs
|
||||
colorgram = with pkgs.python3Packages;
|
||||
buildPythonPackage rec {
|
||||
pname = "colorgram.py";
|
||||
version = "1.2.0";
|
||||
src = fetchPypi {
|
||||
inherit pname version;
|
||||
sha256 = "1gzxgcmg3ndra2j4dg73x8q9dw6b0akj474gxyyhfwnyz6jncxz7";
|
||||
};
|
||||
propagatedBuildInputs = [ pillow ];
|
||||
};
|
||||
colorgramPython = pkgs.python3.withPackages (ps: [ colorgram ]);
|
||||
|
||||
# Pass the wallpaper and any manually selected colors to ./colors.py and
|
||||
# return a JSON file containing the generated colorscheme
|
||||
colorsJSON = pkgs.runCommand "stylix-colors" {
|
||||
colors = builtins.toJSON config.stylix.colors;
|
||||
passAsFile = [ "colors" ];
|
||||
} ''
|
||||
${colorgramPython}/bin/python ${./colors.py} \
|
||||
${cfg.image} < $colorsPath > $out
|
||||
'';
|
||||
|
||||
in {
|
||||
options.stylix.colors = genAttrs [
|
||||
"base00"
|
||||
"base01"
|
||||
"base02"
|
||||
"base03"
|
||||
"base04"
|
||||
"base05"
|
||||
"base06"
|
||||
"base07"
|
||||
"base08"
|
||||
"base09"
|
||||
"base0A"
|
||||
"base0B"
|
||||
"base0C"
|
||||
"base0D"
|
||||
"base0E"
|
||||
"base0F"
|
||||
] (name:
|
||||
mkOption {
|
||||
description = "Hexadecimal color value for ${name}.";
|
||||
default = null;
|
||||
defaultText = "Automatically selected from the background image.";
|
||||
type = types.nullOr (types.strMatching "[0-9a-fA-F]{6}");
|
||||
});
|
||||
|
||||
config.lib.stylix.colors = importJSON colorsJSON;
|
||||
}
|
100
stylix/colors.py
100
stylix/colors.py
|
@ -1,100 +0,0 @@
|
|||
import json
|
||||
import sys
|
||||
import colorgram
|
||||
import colorsys
|
||||
|
||||
|
||||
# Select 9 colors from the image passed on the command line
|
||||
colors = colorgram.extract(sys.argv[1], 9)
|
||||
|
||||
|
||||
# Extract the most dominant color to use as the background
|
||||
colors.sort(key=lambda c: c.proportion)
|
||||
dominant_color = colors.pop(0)
|
||||
|
||||
# Decide whether to generate a light or dark scheme based
|
||||
# on the lightness of the dominant color
|
||||
if dominant_color.hsl.l >= 128:
|
||||
def scale(i):
|
||||
scale = 0.7 - (0.1 * i)
|
||||
return scale * 255
|
||||
|
||||
def clamp(l):
|
||||
return min(l, 100)
|
||||
else:
|
||||
def scale(i):
|
||||
scale = 0.1 + (0.1 * i)
|
||||
return scale * 255
|
||||
|
||||
def clamp(l):
|
||||
return max(l, 155)
|
||||
|
||||
|
||||
def int_to_base(i):
|
||||
return "base{0:02X}".format(i)
|
||||
|
||||
scheme = {}
|
||||
|
||||
# base00 to base07 use the dominant color's hue and saturation,
|
||||
# lightness is a linear scale
|
||||
for i in range(8):
|
||||
scheme[int_to_base(i)] = (
|
||||
dominant_color.hsl.h,
|
||||
scale(i),
|
||||
dominant_color.hsl.s,
|
||||
)
|
||||
|
||||
# base08 to base0A use the remaining 8 colors from the image,
|
||||
# with their lightness clamped to enforce adequate contrast
|
||||
colors.sort(key=lambda c: c.hsl.h) # sort by hue
|
||||
for i in range(8, 16):
|
||||
color = colors[i-8]
|
||||
scheme[int_to_base(i)] = (
|
||||
color.hsl.h,
|
||||
clamp(color.hsl.l),
|
||||
color.hsl.s,
|
||||
)
|
||||
|
||||
# Override with any manually selected colors
|
||||
manual_colors = json.load(sys.stdin)
|
||||
for k, v in manual_colors.items():
|
||||
if v is not None:
|
||||
scheme[k] = colorsys.rgb_to_hls(
|
||||
int(v[0:2], 16) / 255,
|
||||
int(v[2:4], 16) / 255,
|
||||
int(v[4:6], 16) / 255,
|
||||
)
|
||||
scheme[k] = (
|
||||
scheme[k][0] * 255,
|
||||
scheme[k][1] * 255,
|
||||
scheme[k][2] * 255,
|
||||
)
|
||||
|
||||
|
||||
data = {}
|
||||
|
||||
for key, color in scheme.items():
|
||||
r, g, b = colorsys.hls_to_rgb(
|
||||
color[0] / 255,
|
||||
color[1] / 255,
|
||||
color[2] / 255,
|
||||
)
|
||||
data[key + "-dec-r"] = r
|
||||
data[key + "-dec-g"] = g
|
||||
data[key + "-dec-b"] = b
|
||||
data[key + "-rgb-r"] = r * 255
|
||||
data[key + "-rgb-g"] = g * 255
|
||||
data[key + "-rgb-b"] = b * 255
|
||||
|
||||
hex_color = "{0:02x}{1:02x}{2:02x}".format(
|
||||
round(r * 255),
|
||||
round(g * 255),
|
||||
round(b * 255),
|
||||
)
|
||||
data[key + "-hex"] = hex_color
|
||||
data[key + "-hash"] = "#" + hex_color
|
||||
data[key + "-hex-r"] = hex_color[0:2]
|
||||
data[key + "-hex-g"] = hex_color[2:4]
|
||||
data[key + "-hex-b"] = hex_color[4:6]
|
||||
|
||||
json.dump(data, sys.stdout)
|
|
@ -1,13 +0,0 @@
|
|||
{ lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
{
|
||||
imports =
|
||||
[ ./base16.nix ./colors.nix ./fonts.nix ./home-manager.nix ./pixel.nix ];
|
||||
|
||||
options.stylix.image = mkOption {
|
||||
type = types.coercedTo types.package toString types.path;
|
||||
description = "Wallpaper image.";
|
||||
};
|
||||
}
|
55
stylix/palette.nix
Normal file
55
stylix/palette.nix
Normal file
|
@ -0,0 +1,55 @@
|
|||
palette-generator:
|
||||
{ pkgs, lib, config, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.stylix;
|
||||
|
||||
palette = pkgs.runCommand "palette.json" { } ''
|
||||
${palette-generator}/bin/palette-generator ${cfg.image} $out
|
||||
'';
|
||||
|
||||
in {
|
||||
options.stylix = {
|
||||
image = mkOption {
|
||||
type = types.coercedTo types.package toString types.path;
|
||||
description = ''
|
||||
Wallpaper image. This is set as the background of your desktop
|
||||
environment, if possible, and additionally used as the Plymouth splash
|
||||
screen if that is enabled. Colours are automatically selected from the
|
||||
picture to generate the system colour scheme.
|
||||
'';
|
||||
};
|
||||
|
||||
/*
|
||||
TODO: Implement manual palette
|
||||
palette = genAttrs [
|
||||
"base00"
|
||||
"base01"
|
||||
"base02"
|
||||
"base03"
|
||||
"base04"
|
||||
"base05"
|
||||
"base06"
|
||||
"base07"
|
||||
"base08"
|
||||
"base09"
|
||||
"base0A"
|
||||
"base0B"
|
||||
"base0C"
|
||||
"base0D"
|
||||
"base0E"
|
||||
"base0F"
|
||||
] (name:
|
||||
mkOption {
|
||||
description = "Hexadecimal color value for ${name}.";
|
||||
default = null;
|
||||
defaultText = "Automatically selected from the background image.";
|
||||
type = types.nullOr (types.strMatching "[0-9a-fA-F]{6}");
|
||||
});
|
||||
*/
|
||||
};
|
||||
|
||||
config.lib.stylix.colors = importJSON palette;
|
||||
}
|
Loading…
Reference in a new issue