1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-10 14:50:40 +02:00

feat(toplogy): add png and jpg image rendering; add iamge-name small cards

This commit is contained in:
oddlama 2024-03-21 16:31:51 +01:00
parent 5248003dfe
commit 8f66df0238
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
8 changed files with 202 additions and 66 deletions

View file

@ -186,6 +186,47 @@
{
renderer = "d2";
nixosConfigurations = self.nodes;
nodes.fritzbox = {
name = "FritzBox";
deviceType = "router";
hardware.image = ./fritzbox.png;
# interfaces.wan0.network = "internet";
interfaces.wan0.physicalConnections = [
{
node = "ward";
interface = "wan";
}
];
};
nodes.fritzbox-no-img = {
name = "FritzBox No HImg";
deviceType = "router";
interfaces.wan0.physicalConnections = [
{
node = "ward";
interface = "wan";
}
];
};
nodes.fritzbox-device = {
name = "FritzBox No D&HImg";
deviceType = "device";
interfaces.wan0.physicalConnections = [
{
node = "ward";
interface = "wan";
}
];
};
# TODO:
#nodes.fritzbox = config.lib.nodes.mkRouter {};
#nodes.fritzbox = config.lib.nodes.mkSwitch {};
#nodes.fritzbox = config.lib.nodes.mkWifiAP {};
#nodes.printer = config.lib.nodes.mkWifiAP {};
}
];
};

BIN
fritzbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View file

@ -13,7 +13,8 @@ program
.argument("<output>", "output svg")
.option("--font <font>", "Sets the font")
.option("--font-bold <font>", "Sets the bold font")
.option("-w, --width <width>", "Sets the width of the output", 680)
.option("-w, --width <width>", "Sets the width of the output. Use auto for automatic scaling.", "auto")
.option("-h, --height <height>", "Sets the height of the output. Use auto for automatic scaling.", "auto")
.action(async (input, output, options) => {
if (!options.font) {
console.error("--font is required");
@ -27,7 +28,8 @@ program
const markup = html(await fs.readFile(input, { encoding: "utf8" }));
const svg = await satori(markup, {
width: options.width,
...(options.width != "auto" && {width: options.width}),
...(options.height != "auto" && {height: options.height}),
embedFont: true,
fonts: [
{

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -127 1278 1278" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M186.97049 390.020858c249.283591-143.926213 654.058848-143.926213 903.342438 0 249.283591 143.921015 249.283591 377.621133 0 521.542148-249.283591 143.926213-654.058848 143.926213-903.342438 0-249.288789-143.921015-249.288789-377.621133 0-521.542148z" fill="#4467AE" /><path d="M0.005198 368.719633h1277.273022v282.072299H0.005198z" fill="#4467AE" /><path d="M186.97049 107.948559c249.283591-143.926213 654.058848-143.926213 903.342438 0 249.283591 143.921015 249.283591 377.621133 0 521.542148-249.283591 143.926213-654.058848 143.926213-903.342438 0-249.288789-143.921015-249.288789-377.621133 0-521.542148z" fill="#6D8ACA" /><path d="M436.243685 524.263279l57.323062 33.095388-164.5621-6.819719-11.814955-95.008246 57.323063 33.095388 148.037797-85.475194 61.73093 35.642386-148.037797 85.469997zM846.320857 216.221989l-57.323063-33.09019 164.562101 6.819719 11.814954 95.008246-57.323062-33.095388-148.037797 85.469996-61.73093-35.637188 148.037797-85.475195zM445.418078 199.744468l57.323062-33.09019-164.5621 6.819718-11.814955 95.008246 57.323063-33.095388 148.042995 85.469997 61.730929-35.637189L445.418078 199.744468zM865.501316 513.560686l-57.323063 33.095388 164.5621-6.819718 11.814955-95.008246-57.323062 33.095388-148.037797-85.469997-61.73093 35.637189 148.037797 85.469996z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -166 1356 1356" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M0 389.700161h1353.843886v242.365139H0z" fill="#4467AE" /><path d="M711.710196 249.301199c-19.198737-11.087447-48.289414-12.293043-64.920585-2.693674L11.223644 613.552592c-16.626126 9.599369-14.537772 26.397002 4.660965 37.48445l626.244037 361.557718c19.203781 11.087447 48.294458 12.293043 64.920585 2.693674l635.565967-366.945067c16.631171-9.599369 14.542816-26.397002-4.660965-37.484449L711.710196 249.301199z" fill="#4467AE" /><path d="M711.710196 9.170701c-19.198737-11.082403-48.289414-12.293043-64.920585-2.68863L11.223644 373.427138c-16.626126 9.599369-14.537772 26.391958 4.660965 37.479405l626.244037 361.562763c19.203781 11.087447 48.294458 12.293043 64.920585 2.693674l635.565967-366.945067c16.631171-9.604413 14.542816-26.397002-4.660965-37.48445L711.710196 9.170701z" fill="#6D8ACA" /><path d="M296.203321 413.751548l-60.274753 34.800863 12.424196-99.908088 173.04085-7.173044-60.274753 34.800864 187.139765 108.0446-64.915541 37.479405L296.203321 413.751548zM533.21642 276.913886l-60.274752 34.800864 12.424196-99.908089 173.040849-7.173043-60.274752 34.800863 187.139765 108.0446-64.915541 37.474361-187.139765-108.039556z" fill="#FFFFFF" /><path d="M1057.640566 367.888459l60.274752-34.800864-12.424195 99.908089-173.045895 7.173043 60.279797-34.800863-187.139764-108.0446 64.91554-37.479405 187.139765 108.0446zM820.622422 504.731164l60.279797-34.800863-12.424196 99.908089-173.045894 7.173043 60.279797-34.805908-187.139765-108.039556 64.91554-37.479405 187.134721 108.0446z" fill="#FFFFFF" /></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -6,11 +6,13 @@ f: {
inherit
(lib)
attrValues
elem
flatten
flip
literalExpression
mkDefault
mkIf
mkMerge
mkOption
types
;
@ -38,19 +40,33 @@ in
defaultText = literalExpression ''"<name>"'';
};
hardware = {
description = mkOption {
description = "A description of this node's hardware. Usually the device name or lost of the most important components.";
type = types.lines;
default = "";
};
image = mkOption {
description = "An image representing this node, usually shown larger than an icon.";
type = types.nullOr types.path;
default = null;
};
};
icon = mkOption {
description = "The icon representing this node. Usually shown next to the name. Must be a path to an image or a valid icon name (<category>.<name>).";
type = types.nullOr (types.either types.path types.str);
default = null;
};
# FIXME: TODO emoji / icon
# FIXME: TODO hardware description "Odroid H3"
# FIXME: TODO hardware image
deviceType = mkOption {
description = "TODO";
type = types.enum ["nixos" "microvm" "nixos-container"];
description = ''
The device type of the node. This can be set to anything, but some special
values exist that will automatically set some other defaults, most notably
the deviceIcon and preferredRenderType.
'';
type = types.either (types.enum ["nixos" "router" "switch" "device"]) types.str;
};
deviceIcon = mkOption {
@ -64,14 +80,37 @@ in
default = null;
type = types.nullOr types.str;
};
preferredRenderType = mkOption {
description = ''
An optional hint to the renderer to specify whether this node should preferrably
rendered as a full card, or just as an image with name. If there is no hardware
image, this will usually still render a small card.
'';
type = types.enum ["card" "image"];
default = "card";
defaultText = ''"card" # defaults to card but is also derived from the deviceType if possible.'';
};
};
config = {
# Set the default icon, if an icon exists with a matching name
deviceIcon = mkIf (config.topology.isMainModule && config.icons.devices ? ${nodeSubmod.config.deviceType}) (
mkDefault ("devices." + nodeSubmod.config.deviceType)
);
};
config = let
nodeCfg = nodeSubmod.config;
in
mkIf config.topology.isMainModule (mkMerge [
# Set the default icon, if an icon exists with a matching name
{
deviceIcon = mkIf (config.icons.devices ? ${nodeCfg.deviceType}) (
mkDefault ("devices." + nodeCfg.deviceType)
);
}
# If the device type is generic device, try to render as an image
# and set the default image to the deviceIcon.
(mkIf (elem nodeCfg.deviceType ["router" "switch" "device"]) {
preferredRenderType = mkDefault "image";
hardware.image = mkDefault (config.lib.icons.get nodeCfg.deviceIcon);
})
]);
}));
};

View file

@ -1,3 +1,13 @@
# TODO:
# - disks (from disko) + render
# - hardware info (image small top and image big bottom and full (no card), maybe just image and render position)
# - render router and other devices (card with interfaces, card with just image)
# - render nodes with guests, guests in short form
# - nginx proxy pass render, with upstream support
# - more service info
# - impermanence render?
# - stable pseudorandom colors from palette with no-reuse until necessary
# - search todo and do
{
config,
lib,
@ -19,18 +29,25 @@
types
;
htmlToSvgCommand = inFile: outFile: ''
fileBase64 = file: let
out = pkgs.runCommand "base64" {} ''
${pkgs.coreutils}/bin/base64 -w0 < ${file} > $out
'';
in "${out}";
htmlToSvgCommand = inFile: outFile: args: ''
${lib.getExe pkgs.html-to-svg} \
--font ${pkgs.jetbrains-mono}/share/fonts/truetype/JetBrainsMono-Regular.ttf \
--font-bold ${pkgs.jetbrains-mono}/share/fonts/truetype/JetBrainsMono-Bold.ttf \
--width 680 \
--width ${toString (args.width or "auto")} \
--height ${toString (args.height or "auto")} \
${inFile} ${outFile}
'';
renderHtmlToSvg = html: name: let
renderHtmlToSvg = card: name: let
drv = pkgs.runCommand "generate-svg-${name}" {} ''
mkdir -p $out
${htmlToSvgCommand (pkgs.writeText "${name}.html" html) "$out/${name}.svg"}
${htmlToSvgCommand (pkgs.writeText "${name}.html" card.html) "$out/${name}.svg" card}
'';
in "${drv}/${name}.svg";
@ -46,13 +63,13 @@
content = head (splitString "</svg>" withoutPrefix);
in ''<svg tw="${twAttrs}" ${content}</svg>''
else if hasSuffix ".png" file
# FIXME: TODO png, jpg, ...
then ''
<img tw="object-contain ${twAttrs}" src="data:image/png;base64,${"TODO"}/>"
''
then ''<img tw="object-contain ${twAttrs}" src="data:image/png;base64,${builtins.readFile fileBase64 file}/>"''
else if hasSuffix ".jpg" file || hasSuffix ".jpeg" file
then ''<img tw="object-contain ${twAttrs}" src="data:image/jpeg;base64,${builtins.readFile fileBase64 file}/>"''
else builtins.throw "Unsupported icon file type: ${file}";
mkImageMaybe = twAttrs: file: optionalString (file != null) (mkImage twAttrs file);
mkImageMaybeIf = cond: twAttrs: file: optionalString (cond && file != null) (mkImage twAttrs file);
mkImageMaybe = mkImageMaybeIf true;
mkSpacer = name:
/*
@ -68,26 +85,19 @@
</div>
'';
mkRootContainer = contents:
mkRootContainer = twAttrs: contents:
/*
html
*/
''
<div tw="flex flex-col w-full h-full items-center">
<div tw="flex flex-col w-full h-full text-[#e3e6eb] font-mono ${twAttrs}" style="font-family: 'JetBrains Mono'">
${contents}
</div>
</div>
'';
mkRootCard = twAttrs: contents:
mkRootContainer
/*
html
*/
''
<div tw="flex flex-col w-full h-full bg-[#101419] text-[#e3e6eb] font-mono ${twAttrs}" style="font-family: 'JetBrains Mono'">
${contents}
</div>
'';
mkCardContainer = mkRootContainer "bg-[#101419] rounded-xl";
spacingMt2 = ''
<div tw="flex mt-2"></div>
@ -161,36 +171,73 @@
</div>
'';
mkInfoCardNetwork = node:
mkRootCard "rounded-xl"
/*
html
*/
''
${mkTitle node}
mkNetCard = node: {
width = 680;
html =
mkCardContainer
/*
html
*/
''
${mkTitle node}
${concatLines (map mkInterface (attrValues node.interfaces))}
${optionalString (node.interfaces != {}) spacingMt2}
'';
${concatLines (map mkInterface (attrValues node.interfaces))}
${optionalString (node.interfaces != {}) spacingMt2}
mkInfoCardFull = node: let
${mkImageMaybe "w-full h-24" node.hardware.image}
'';
};
mkCard = node: let
services = filter (x: !x.hidden) (attrValues node.services);
in
mkRootCard "rounded-xl"
/*
html
*/
''
${mkTitle node}
in {
width = 680;
html =
mkCardContainer
/*
html
*/
''
${mkTitle node}
${concatLines (map mkInterface (attrValues node.interfaces))}
${optionalString (node.interfaces != {}) spacingMt2}
${concatLines (map mkInterface (attrValues node.interfaces))}
${optionalString (node.interfaces != {}) spacingMt2}
${concatLines (map mkService services)}
${optionalString (services != []) spacingMt2}
${concatLines (map mkService services)}
${optionalString (services != []) spacingMt2}
<div tw="flex mb-2"></div>
'';
${mkImageMaybe "w-full h-24" node.hardware.image}
'';
};
mkImageWithName = node: {
html = let
deviceIconImage = config.lib.icons.get node.deviceIcon;
in
mkRootContainer ""
/*
html
*/
''
<div tw="flex flex-row mx-6 mt-2 items-center">
${mkImageMaybe "w-12 h-12 mr-4" (config.lib.icons.get node.icon)}
<h2 tw="grow text-4xl font-bold">${node.name}</h2>
<div tw="flex grow"></div>
<h2 tw="text-4xl">${node.deviceType}</h2>
${mkImageMaybeIf (node.hardware.image != null -> deviceIconImage != node.hardware.image) "w-16 h-16 ml-4" deviceIconImage}
</div>
${mkImageMaybe "w-full h-24" node.hardware.image}
'';
};
mkPreferredRender = node:
(
if node.preferredRenderType == "image" && node.hardware.image != null
then mkImageWithName
else mkCard
)
node;
};
};
in {
@ -206,18 +253,16 @@ in {
config = {
lib.renderers.svg.node = {
mkInfoCardNetwork = node: renderHtmlToSvg (html.node.mkInfoCardNetwork node) "card-network-${node.name}";
mkInfoCardFull = node: renderHtmlToSvg (html.node.mkInfoCardFull node) "card-node-${node.name}";
mkNetCard = node: renderHtmlToSvg (html.node.mkNetCard node) "card-network-${node.id}";
mkCard = node: renderHtmlToSvg (html.node.mkCard node) "card-node-${node.id}";
mkPreferredRender = node: renderHtmlToSvg (html.node.mkPreferredRender node) "preferred-render-node-${node.id}";
};
renderers.svg.output = pkgs.runCommand "topology-svgs" {} ''
mkdir -p $out/nodes
${concatLines (flip map (attrValues config.nodes) (
node:
htmlToSvgCommand (
pkgs.writeText "node-${node.name}.html" (html.node.mkInfoCardFull node)
) "$out/nodes/${node.name}.svg"
))}
${concatLines (flip map (attrValues config.nodes) (node: ''
cp ${config.lib.renderers.svg.node.mkPreferredRender node} $out/nodes/${node.id}.svg
''))}
'';
};
}