Skip to content

Commit 7ade339

Browse files
committed
matrix-icons: init at 0.1.0
A simple Typst builder to produce SVG matrix room icons. New rooms are automatically detected from their respective icon.json files. Signed-off-by: Fernando Rodrigues <[email protected]>
1 parent 7d8124c commit 7ade339

File tree

10 files changed

+325
-0
lines changed

10 files changed

+325
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# NixOS Matrix Room Icons
2+
3+
This directory contains the builder scripts and the sources that generate the icons for NixOS-managed Matrix rooms.
4+
5+
## Creating a New Room Icon
6+
7+
1. Create a new subdirectory inside the `rooms/` directory with your room's alias in the [nixos.org](https://matrix.to/#/#community:nixos.org) space.
8+
9+
- For example, if your new room can be reached with the `#haskell:nixos.org` address, you would name the subdirectory `haskell`.
10+
11+
2. Inside your new subdirectory, create a file named `icon.json`. This file determines how your icon will be generated from the default templates.
12+
13+
- See the "`icon.json` Syntax" section below for more information on the valid fields for the `icon.json` file.
14+
15+
3. Build your new icon with `nix build .#matrix-icons.your-room-name`. Following the same example above, you would build the `#haskell:nixos.org` room icon with `nix build .#matrix-icons.haskell`.
16+
17+
- You can also build all room icons at once using `nix build .#matrix-icons`.
18+
19+
## `icon.json` Syntax
20+
21+
<!-- TODO: @sigmasquadron: How to best expose the full schema for `icon.json`? It would be great if there was some magic hidden file that code editors picked up automatically or something. -->
22+
23+
## Custom Assets
24+
25+
<!-- TODO: @sigmasquadron: Explain how to add .svg images to be embedded in the room icon. -->
26+
27+
## Examples
28+
29+
<!-- TODO: @sigmasquadron: Add a table with examples. -->
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Date must be set to none for deterministic builds
2+
#set document(date: none)
3+
#set page(margin: 0pt, width: 2048pt, height: 2048pt)
4+
5+
#let data = json("final.json")
6+
7+
#let palette-colour(intOrStr) = if type(intOrStr) == int {
8+
if intOrStr == 0 { luma(0) } else if intOrStr == 1 {
9+
luma(255)
10+
} else if intOrStr == 2 { rgb("#5b7ec8") } else if intOrStr == 3 {
11+
rgb("#87adfa")
12+
} else if intOrStr == 4 { rgb("#5fb8f2") } else if intOrStr == 5 {
13+
rgb("#aaa1f6")
14+
} else if intOrStr == 6 { rgb("#e99861") } else if intOrStr == 7 {
15+
rgb("#f08d94")
16+
} else if intOrStr == 8 { rgb("#d991d2") } else if intOrStr == 9 {
17+
rgb("#6fc488")
18+
} else { panic("Invalid Colour ID: " + intOrStr) }
19+
} else { rgb(intOrStr) }
20+
21+
// https://typst.app/universe/package/one-liner
22+
#let fit-to-width(max-text-size: 800pt, min-text-size: 256pt, it) = context {
23+
let contentsize = measure(it)
24+
layout(size => {
25+
if contentsize.width > 0pt {
26+
// Prevent failure on empty content
27+
let ratio-x = size.width / contentsize.width
28+
let ratio-y = size.height / contentsize.height
29+
let ratio = if ratio-x < ratio-y {
30+
ratio-x
31+
} else {
32+
ratio-y
33+
}
34+
35+
let newx = contentsize.width * ratio
36+
let newy = contentsize.height * ratio
37+
let suggestedtextsize = 1em * ratio
38+
if (suggestedtextsize + 0pt).to-absolute() > max-text-size {
39+
suggestedtextsize = max-text-size
40+
}
41+
if (suggestedtextsize + 0pt).to-absolute() < min-text-size {
42+
suggestedtextsize = min-text-size
43+
}
44+
set text(size: suggestedtextsize)
45+
it
46+
}
47+
})
48+
}
49+
50+
#let icon(
51+
type: none,
52+
colours: none,
53+
contents: none,
54+
) = {
55+
set page(
56+
fill: palette-colour(if type == "standard" { colours.background } else {
57+
colours.border
58+
}),
59+
background: {
60+
if type == "standard" {
61+
grid(
62+
columns: 1,
63+
rows: (992pt, 64pt, 992pt),
64+
gutter: 0pt,
65+
rect(
66+
width: 100%,
67+
height: 100%,
68+
stroke: none,
69+
inset: if contents.style == "text" { 128pt } else { 0pt },
70+
if contents.style == "text" {
71+
align(center + bottom, fit-to-width(text(
72+
font: "Route 159",
73+
weight: "bold",
74+
fill: palette-colour(colours.text),
75+
contents.text,
76+
)))
77+
} else if contents.style == "image" {
78+
align(center + bottom, image(
79+
{ "icons/" + contents.image + ".svg" },
80+
height: 992pt,
81+
fit: "contain",
82+
))
83+
} else { panic("Invalid style: " + content.style) },
84+
),
85+
86+
rect(width: 100%, height: 100%, fill: palette-colour(colours.border)),
87+
88+
rect(
89+
width: 100%,
90+
height: 100%,
91+
fill: palette-colour(2),
92+
inset: 0pt,
93+
image(
94+
"icons/nixos-logomark-white-flat-minimal.svg",
95+
fit: "contain",
96+
),
97+
),
98+
)
99+
} else {
100+
circle(
101+
radius: 768pt,
102+
fill: palette-colour(colours.background),
103+
inset: -224pt,
104+
image({ "icons/" + contents.image + ".svg" }, fit: "contain"),
105+
)
106+
}
107+
},
108+
)
109+
pagebreak(weak: true)
110+
// FIXME: For some reason, the icons aren't generated correctly if there isn't *something* in the first page.
111+
text(fill: rgb("#00000000"), [.])
112+
}
113+
114+
#if sys.inputs.singleIcon == "null" {
115+
for room in data.values() {
116+
icon(type: room.type, colours: room.colours, contents: room.contents)
117+
}
118+
} else {
119+
icon(
120+
type: data.at(sys.inputs.singleIcon).type,
121+
colours: data.at(sys.inputs.singleIcon).colours,
122+
contents: data.at(sys.inputs.singleIcon).contents,
123+
)
124+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{
2+
jq,
3+
jura,
4+
lib,
5+
matrix-icons,
6+
nixos-branding,
7+
efficient-compression-tool,
8+
route159,
9+
symlinkJoin,
10+
typix-lib,
11+
12+
singleIcon ? null,
13+
}:
14+
15+
let
16+
inherit (builtins)
17+
readDir
18+
;
19+
inherit (lib.asserts)
20+
assertOneOf
21+
;
22+
inherit (lib.attrsets)
23+
attrNames
24+
attrValues
25+
genAttrs
26+
removeAttrs
27+
;
28+
inherit (lib.fileset)
29+
toSource
30+
unions
31+
;
32+
inherit (typix-lib)
33+
buildDeterministicTypstProject
34+
;
35+
36+
typstSource = "main.typ";
37+
listOfRooms = attrNames (readDir ./rooms);
38+
in
39+
40+
assert assertOneOf "The selected room icon" singleIcon ([ null ] ++ listOfRooms);
41+
42+
buildDeterministicTypstProject {
43+
pname = "matrix-icons";
44+
version = "0.1.0";
45+
46+
src = toSource {
47+
root = ./.;
48+
fileset = unions [ ./main.typ ];
49+
};
50+
51+
nativeBuildInputs = [
52+
jq
53+
efficient-compression-tool
54+
];
55+
56+
inherit typstSource;
57+
58+
fontPaths = [
59+
"${jura}/share/fonts/truetype/jura"
60+
"${route159}/share/fonts/opentype/route159"
61+
];
62+
63+
virtualPaths = [
64+
{
65+
dest = "icons";
66+
src = "${symlinkJoin {
67+
name = "helper-icons";
68+
paths = (
69+
attrValues (
70+
removeAttrs nixos-branding.artifacts.media-kit [
71+
"callPackage"
72+
"newScope"
73+
"overrideScope"
74+
"packages"
75+
"recurseForDerivations"
76+
]
77+
)
78+
);
79+
postBuild = ''
80+
find ${./rooms} -type f -name "*.svg" -exec cp {} $out \;
81+
'';
82+
}}";
83+
}
84+
];
85+
86+
buildPhaseTypstCommand = ''
87+
find ${./rooms} -type f -name icon.json -print0 | xargs -0 jq -n 'reduce inputs as $item ({}; . + { (input_filename | split("/")[4]): $item })' >> final.json
88+
typst compile \
89+
--format png \
90+
--input singleIcon=${if (isNull singleIcon) then "null" else singleIcon} \
91+
${typstSource} "{p}.png"
92+
'';
93+
94+
installPhaseCommand = ''
95+
mkdir $out
96+
${
97+
if (isNull singleIcon) then
98+
''
99+
paste \
100+
<(jq -r 'keys[]' final.json) \
101+
<(find . -maxdepth 1 -type f -name '*.png' -printf '%f\n') \
102+
| while IFS=$'\t' read key png; do
103+
mv -- "$png" "$out/$key.png"
104+
done
105+
''
106+
else
107+
"mv 1.png $out/${singleIcon}.png"
108+
}
109+
ect -9 -keep --strip --strict $out
110+
'';
111+
112+
passthru = genAttrs listOfRooms (name: matrix-icons.override { singleIcon = name; });
113+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!*.svg
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "standard",
3+
"colours": {
4+
"background": 1,
5+
"border": 3,
6+
"text": 2
7+
},
8+
"contents": {
9+
"style": "text",
10+
"text": "dev"
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"type": "standard",
3+
"colours": {
4+
"background": 4,
5+
"border": 1
6+
},
7+
"contents": {
8+
"style": "image",
9+
"image": "kde"
10+
}
11+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "standard",
3+
"colours": {
4+
"background": 1,
5+
"border": 6,
6+
"text": 4
7+
},
8+
"contents": {
9+
"style": "text",
10+
"text": "arm"
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "standard",
3+
"colours": {
4+
"background": 7,
5+
"border": "#FF0000",
6+
"text": "#991111"
7+
},
8+
"contents": {
9+
"style": "text",
10+
"text": "#@!%"
11+
}
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"type": "main",
3+
"colours": {
4+
"background": 1,
5+
"border": 2
6+
},
7+
"contents": {
8+
"image": "nixos-logomark-default-gradient-minimal"
9+
}
10+
}

0 commit comments

Comments
 (0)