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:
parent
5248003dfe
commit
8f66df0238
8 changed files with 202 additions and 66 deletions
41
flake.nix
41
flake.nix
|
@ -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
BIN
fritzbox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
|
@ -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: [
|
||||
{
|
||||
|
|
3
topology/icons/devices/cloud-server.svg
Normal file
3
topology/icons/devices/cloud-server.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
3
topology/icons/devices/router.svg
Normal file
3
topology/icons/devices/router.svg
Normal 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 |
3
topology/icons/devices/switch.svg
Normal file
3
topology/icons/devices/switch.svg
Normal 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 |
|
@ -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);
|
||||
})
|
||||
]);
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
''))}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue