diff --git a/flake.nix b/flake.nix index 432675d..13da006 100644 --- a/flake.nix +++ b/flake.nix @@ -223,12 +223,12 @@ networks.home-lan = { name = "Home LAN"; cidrv4 = "192.168.1.0/24"; - color = "#78dba9"; + #color = "#78dba9"; }; networks.home-fritzbox = { name = "Home Fritzbox"; cidrv4 = "192.168.178.0/24"; - color = "#f1cf8a"; + #color = "#f1cf8a"; }; nodes.ward.interfaces.lan.network = "home-lan"; diff --git a/topology/options/interfaces.nix b/topology/options/interfaces.nix index 63909a2..f040818 100644 --- a/topology/options/interfaces.nix +++ b/topology/options/interfaces.nix @@ -6,8 +6,6 @@ f: { }: let inherit (lib) - all - assertMsg attrNames attrValues concatLines @@ -18,51 +16,26 @@ f: { flatten flip foldl' - isAttrs length mapAttrsToList mkDefault mkIf mkOption - mkOptionType optional optionalAttrs recursiveUpdate reverseList - showOption types unique warnIf ; - # Checks whether the value is a lazy value without causing - # it's value to be evaluated - isLazyValue = x: isAttrs x && x ? _lazyValue; - # Constructs a lazy value holding the given value. - lazyValue = value: {_lazyValue = value;}; - - # Represents a lazy value of the given type, which - # holds the actual value as an attrset like { _lazyValue = ; }. - # This allows the option to be defined and filtered from a defintion - # list without evaluating the value. - lazyValueOf = type: - mkOptionType rec { - name = "lazyValueOf ${type.name}"; - inherit (type) description descriptionClass emptyValue getSubOptions getSubModules; - check = isLazyValue; - merge = loc: defs: - assert assertMsg - (all (x: type.check x._lazyValue) defs) - "The option `${showOption loc}` is defined with a lazy value holding an invalid type"; - types.mergeOneOption loc defs; - substSubModules = m: types.uniq (type.substSubModules m); - functor = (types.defaultFunctor name) // {wrapped = type;}; - nestedTypes.elemType = type; - }; - - # Represents a value or lazy value of the given type that will - # automatically be coerced to the given type when merged. - lazyOf = type: types.coercedTo (lazyValueOf type) (x: x._lazyValue) type; + inherit + (import ../topology/lazy.nix lib) + isLazyValue + lazyOf + lazyValue + ; allNodes = attrNames config.nodes; allInterfacesOf = node: attrNames config.nodes.${node}.interfaces; @@ -323,6 +296,12 @@ in description = "The other node's interface id."; type = types.str; }; + + renderer.reverse = mkOption { + description = "Whether to reverse the edge. Can be useful to affect node positioning if the layouter is directional."; + type = types.bool; + default = false; + }; }; }); }; diff --git a/topology/options/networks.nix b/topology/options/networks.nix index 188c398..82526c4 100644 --- a/topology/options/networks.nix +++ b/topology/options/networks.nix @@ -1,16 +1,101 @@ f: { lib, config, + options, ... }: let inherit (lib) + attrNames attrValues + elemAt flatten flip + foldl' + imap0 + length + mapAttrsToList mkOption + mod + optional + optionalAttrs + recursiveUpdate + subtractLists types + unique + warnIf ; + + inherit + (import ../topology/lazy.nix lib) + isLazyValue + lazyOf + lazyValue + ; + + style = primaryColor: secondaryColor: pattern: { + inherit primaryColor secondaryColor pattern; + }; + + predefinedStyles = [ + (style "#f1cf8a" null "solid") + (style "#70a5eb" null "solid") + (style "#9dd68d" null "solid") + (style "#5fe1ff" null "solid") + (style "#e05f65" null "solid") + (style "#f9a872" null "solid") + (style "#78dba9" null "solid") + (style "#9378de" null "solid") + (style "#c68aee" null "solid") + (style "#f5a6b8" null "solid") + (style "#70a5eb" null "dashed") + (style "#9dd68d" null "dashed") + (style "#f1cf8a" null "dashed") + (style "#5fe1ff" null "dashed") + (style "#e05f65" null "dashed") + (style "#f9a872" null "dashed") + (style "#78dba9" null "dashed") + (style "#9378de" null "dashed") + (style "#c68aee" null "dashed") + (style "#f5a6b8" null "dashed") + (style "#e05f65" "#e3e6eb" "dashed") + (style "#70a5eb" "#e3e6eb" "dashed") + (style "#9378de" "#e3e6eb" "dashed") + (style "#9dd68d" "#707379" "dashed") + (style "#f1cf8a" "#707379" "dashed") + (style "#5fe1ff" "#707379" "dashed") + (style "#f9a872" "#707379" "dashed") + (style "#78dba9" "#707379" "dashed") + (style "#c68aee" "#707379" "dashed") + (style "#f5a6b8" "#707379" "dashed") + ]; + + # A map containing all networks that have an explicitly assigned style, and the style. + explicitStyles = foldl' recursiveUpdate {} (flatten ( + flip map options.networks.definitions (mapAttrsToList ( + netId: net: + optional (net ? style && !isLazyValue net.style) { + ${netId} = net.style; + } + )) + )); + + # All unused predefined styles + remainingStyles = subtractLists (unique (attrValues explicitStyles)) predefinedStyles; + # All networks without styles + remainingNets = subtractLists (attrNames explicitStyles) (attrNames config.networks); + # Fold over all networks that have no style and assign the next free one. Warn and repeat from beginning if necessary. + computedStyles = + explicitStyles + // warnIf + (length remainingNets > length remainingStyles) + "topology: There are more networks without styles than predefined styles. Some styles will have to be reused!" + ( + foldl' recursiveUpdate {} (imap0 (i: net: { + ${net} = elemAt remainingStyles (mod i (length remainingStyles)); + }) + remainingNets) + ); in f { options.networks = mkOption { @@ -37,15 +122,20 @@ in default = null; }; - color = mkOption { - description = "The color of this network"; - default = "random"; - apply = x: - if x == "random" - then "#ff00ff" # FIXME: TODO: lookuptable linear probing hashing into palette - else x; - type = types.either (types.strMatching "^#[0-9a-f]{6}$") (types.enum ["random"]); - }; + style = + mkOption { + description = '' + A style for this network, usually used to draw connections. + Must be an attrset consisting of three attributes: + - primaryColor (#rrggbb): The primary color, usually the color of edges. + - secondaryColor (#rrggbb): The secondary color, usually the background of a dashed line and only shown when pattern != solid. Set to null for transparent. + - pattern (solid, dashed, dotted): The pattern to use. + ''; + type = lazyOf types.attrs; + } + // optionalAttrs config.topology.isMainModule { + default = lazyValue computedStyles.${networkSubmod.config.id}; + }; cidrv4 = mkOption { description = "The CIDRv4 address space of this network or null if it doesn't use ipv4"; diff --git a/topology/topology/lazy.nix b/topology/topology/lazy.nix new file mode 100644 index 0000000..8c68a90 --- /dev/null +++ b/topology/topology/lazy.nix @@ -0,0 +1,42 @@ +lib: let + inherit + (lib) + all + assertMsg + isAttrs + mkOptionType + showOption + types + ; + + # Checks whether the value is a lazy value without causing + # it's value to be evaluated + isLazyValue = x: isAttrs x && x ? _lazyValue; + # Constructs a lazy value holding the given value. + lazyValue = value: {_lazyValue = value;}; + + # Represents a lazy value of the given type, which + # holds the actual value as an attrset like { _lazyValue = ; }. + # This allows the option to be defined and filtered from a defintion + # list without evaluating the value. + lazyValueOf = type: + mkOptionType rec { + name = "lazyValueOf ${type.name}"; + inherit (type) description descriptionClass emptyValue getSubOptions getSubModules; + check = isLazyValue; + merge = loc: defs: + assert assertMsg + (all (x: type.check x._lazyValue) defs) + "The option `${showOption loc}` is defined with a lazy value holding an invalid type"; + types.mergeOneOption loc defs; + substSubModules = m: types.uniq (type.substSubModules m); + functor = (types.defaultFunctor name) // {wrapped = type;}; + nestedTypes.elemType = type; + }; + + # Represents a value or lazy value of the given type that will + # automatically be coerced to the given type when merged. + lazyOf = type: types.coercedTo (lazyValueOf type) (x: x._lazyValue) type; +in { + inherit isLazyValue lazyValue lazyValueOf lazyOf; +} diff --git a/topology/topology/renderers/elk/default.nix b/topology/topology/renderers/elk/default.nix index 81b1c21..4e1be48 100644 --- a/topology/topology/renderers/elk/default.nix +++ b/topology/topology/renderers/elk/default.nix @@ -54,14 +54,17 @@ else value ); - mkEdge = from: to: extra: { - edges."${from}__${to}" = - { - sources = [from]; - targets = [to]; - } - // extra; - }; + mkEdge = from: to: reverse: extra: + if reverse + then mkEdge to from false extra + else { + edges."${from}__${to}" = + { + sources = [from]; + targets = [to]; + } + // extra; + }; mkPort = recursiveUpdate { width = 8; @@ -81,6 +84,32 @@ // extraStyle; }; + pathStyleFromNetworkStyle = style: + { + solid = { + stroke = style.primaryColor; + }; + dashed = + { + stroke = style.primaryColor; + stroke-dasharray = "10,8"; + stroke-linecap = "round"; + } + // optionalAttrs (style.secondaryColor != null) { + background = style.secondaryColor; + }; + dotted = + { + stroke = style.primaryColor; + stroke-dasharray = "2,6"; + stroke-linecap = "round"; + } + // optionalAttrs (style.secondaryColor != null) { + background = style.secondaryColor; + }; + } + .${style.pattern}; + netToElk = net: [ { children.network.children."net:${net.id}" = { @@ -100,15 +129,18 @@ idForInterface = node: interfaceId: "children.node:${node.id}.ports.interface:${interfaceId}"; nodeInterfaceToElk = node: interface: let + netStyle = optionalAttrs (interface.network != null) { + fill = config.networks.${interface.network}.style.primaryColor; + }; interfaceLabels = { "00-name" = mkLabel interface.id 1 {}; } // optionalAttrs (interface.mac != null) { - "50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";}; + "50-mac" = mkLabel interface.mac 1 netStyle; } // optionalAttrs (interface.addresses != []) { - "60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";}; + "60-addrs" = mkLabel (toString interface.addresses) 1 netStyle; }; in [ @@ -131,8 +163,8 @@ # Edge in network-centric view (optionalAttrs (interface.network != null) ( - mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}.ports.default" { - style.stroke = config.networks.${interface.network}.color; + mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}.ports.default" false { + style = pathStyleFromNetworkStyle config.networks.${interface.network}.style; } )) ] @@ -149,10 +181,11 @@ mkEdge (idForInterface node interface.id) (idForInterface config.nodes.${conn.node} conn.interface) + conn.renderer.reverse { - style = optionalAttrs (interface.network != null) { - stroke = config.networks.${interface.network}.color; - }; + style = optionalAttrs (interface.network != null) ( + pathStyleFromNetworkStyle config.networks.${interface.network}.style + ); } ) ) @@ -196,7 +229,7 @@ labels."00-name" = mkLabel "guests" 1 {}; }; } - // mkEdge "children.node:${node.parent}.ports.guests" "children.node:${node.id}" { + // mkEdge "children.node:${node.parent}.ports.guests" "children.node:${node.id}" false { style.stroke-dasharray = "10,8"; style.stroke-linecap = "round"; } diff --git a/topology/topology/renderers/svg/default.nix b/topology/topology/renderers/svg/default.nix index 0bbc5bc..3ad788f 100644 --- a/topology/topology/renderers/svg/default.nix +++ b/topology/topology/renderers/svg/default.nix @@ -113,10 +113,18 @@ mkCard = net: { width = 480; html = let - netColor = - if net.color != null - then net.color - else "#b6beca"; + netStylePreview = let + secondaryColor = + if net.style.secondaryColor == null + then "#00000000" + else net.style.secondaryColor; + in + { + solid = ''
''; + dashed = ''
''; + dotted = ''
''; + } + .${net.style.pattern}; in mkCardContainer /* @@ -124,7 +132,7 @@ */ ''
-
+ ${netStylePreview}

${net.name}

${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get net.icon)}