feat(clients): the OS marks tell gaming distros apart, and Windows looks current

Three things were wrong with the host-card OS icons.

The Windows mark was Font Awesome 5's, which is still the Windows 8/10 flag
with the perspective skew — dated next to the flat four-pane mark Microsoft
has shipped since Windows 11. No icon set has the current one (Simple Icons
carries no windows/microsoft slug at all), so it is drawn here: four equal
squares at the authentic 11.377 + 1.246 proportion. The Decky plugin was
pulling FaWindows straight from react-icons, so it now inlines the masters
like the web console does, or it would have kept the old flag regardless.

Bazzite, CachyOS and Nobara collapsed onto their family's mark. The host
already advertises the full chain, so this is purely missing art: all three
now ship a leaf mark, because "a Bazzite box" and "a Fedora box" are
different machines to the person reading the card. CachyOS and Nobara come
from Simple Icons; Bazzite has no icon anywhere, so its "b" is lifted out of
the project's own badge (Apache-2.0, attributed).

On Android every non-square mark was stretched. A VectorPainter maps the
viewport onto the ImageVector's default size with independent x and y
scales, so declaring a 448x512 Tux as 24x24 dp squashed it — silently, no
crash, no warning. The longest viewport edge now sets the 24 dp box and the
other follows the ratio, which is what Icon()'s ContentScale.Fit expects.
A unit test pins the invariant; every other client was already correct.

Also: scripts/gen-os-icons.sh replaces the undocumented hand-run pipeline
that turns a master into the GTK symbolic SVG, the Windows PNG and the Apple
template PDF. It reproduces the committed artifacts byte-identically.

Verified: web and Decky typecheck, Decky bundles, Android compiles and its
tests pass, PunktfunkKit builds, osinfo's tests pass. Not verified on glass,
and the GTK/Windows client crates do not build on macOS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:17:02 +02:00
co-authored by Claude Opus 5
parent f4f83202cb
commit 5520167958
37 changed files with 500 additions and 87 deletions
+25 -4
View File
@@ -18,6 +18,7 @@ VENDORED THIRD-PARTY SOURCE (inside first-party crates)
Vulkan-Headers (vendored, crates/pyrowave-sys) — https://github.com/KhronosGroup/Vulkan-Headers
Font Awesome Free brand icons (vendored, assets/os-icons) — https://fontawesome.com
Simple Icons (vendored, assets/os-icons) — https://simpleicons.org
Bazzite logo (vendored, assets/os-icons) — https://github.com/ublue-os/bazzite
----------------------------------------------------------------------------
MANIFEST (crate version — SPDX license — source)
@@ -2930,6 +2931,25 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
----------------------------------------------------------------------------
The following license (bazzite.txt) applies to: Bazzite logo (vendored, assets/os-icons)
----------------------------------------------------------------------------
Bazzite — the `bazzite` mark in assets/os-icons/ is derived from the Bazzite logo in
the Bazzite source repository (repo_content/Bazzite.svg).
Copyright (c) Universal Blue (https://github.com/ublue-os/bazzite)
Licensed under the Apache License, Version 2.0,
https://www.apache.org/licenses/LICENSE-2.0.
Modifications: the logo's "b" letterform was lifted out of the surrounding badge, the
gradient and decorative overlays were dropped, and the path was translated and scaled
into a 24x24 box with a monochrome fill (fill="currentColor").
Brand icons are trademarks of their respective owners and are used for identification
purposes only; their use does not imply endorsement.
----------------------------------------------------------------------------
The following license (LICENSE) applies to: bindgen 0.72.1
----------------------------------------------------------------------------
@@ -6283,8 +6303,8 @@ the following restrictions:
----------------------------------------------------------------------------
The following license (font-awesome-brands.txt) applies to: Font Awesome Free brand icons (vendored, assets/os-icons)
----------------------------------------------------------------------------
Font Awesome Free — brand icons (windows, apple, linux, steam, ubuntu, fedora,
opensuse in assets/os-icons/) are from Font Awesome Free.
Font Awesome Free — brand icons (apple, linux, steam, ubuntu, fedora, opensuse in
assets/os-icons/) are from Font Awesome Free.
Copyright (c) Fonticons, Inc. (https://fontawesome.com)
@@ -12997,8 +13017,9 @@ SOFTWARE.
----------------------------------------------------------------------------
The following license (simple-icons.txt) applies to: Simple Icons (vendored, assets/os-icons)
----------------------------------------------------------------------------
Simple Icons — brand icons (arch, nixos, debian in assets/os-icons/) are from
Simple Icons (https://simpleicons.org, https://github.com/simple-icons/simple-icons).
Simple Icons — brand icons (arch, nixos, debian, cachyos, nobara in assets/os-icons/)
are from Simple Icons
(https://simpleicons.org, https://github.com/simple-icons/simple-icons).
The Simple Icons SVG path data is released under CC0 1.0 Universal (public domain
dedication), https://creativecommons.org/publicdomain/zero/1.0/ — no attribution
+14
View File
@@ -0,0 +1,14 @@
Bazzite — the `bazzite` mark in assets/os-icons/ is derived from the Bazzite logo in
the Bazzite source repository (repo_content/Bazzite.svg).
Copyright (c) Universal Blue (https://github.com/ublue-os/bazzite)
Licensed under the Apache License, Version 2.0,
https://www.apache.org/licenses/LICENSE-2.0.
Modifications: the logo's "b" letterform was lifted out of the surrounding badge, the
gradient and decorative overlays were dropped, and the path was translated and scaled
into a 24x24 box with a monochrome fill (fill="currentColor").
Brand icons are trademarks of their respective owners and are used for identification
purposes only; their use does not imply endorsement.
@@ -1,5 +1,5 @@
Font Awesome Free — brand icons (windows, apple, linux, steam, ubuntu, fedora,
opensuse in assets/os-icons/) are from Font Awesome Free.
Font Awesome Free — brand icons (apple, linux, steam, ubuntu, fedora, opensuse in
assets/os-icons/) are from Font Awesome Free.
Copyright (c) Fonticons, Inc. (https://fontawesome.com)
+3 -2
View File
@@ -1,5 +1,6 @@
Simple Icons — brand icons (arch, nixos, debian in assets/os-icons/) are from
Simple Icons (https://simpleicons.org, https://github.com/simple-icons/simple-icons).
Simple Icons — brand icons (arch, nixos, debian, cachyos, nobara in assets/os-icons/)
are from Simple Icons
(https://simpleicons.org, https://github.com/simple-icons/simple-icons).
The Simple Icons SVG path data is released under CC0 1.0 Universal (public domain
dedication), https://creativecommons.org/publicdomain/zero/1.0/ — no attribution
+32 -10
View File
@@ -7,7 +7,7 @@ Android `ImageVector`s). One file per **icon token** of the host's OS-identity c
| token | mark | source |
|---|---|---|
| `windows` | Windows | Font Awesome Free brands (CC BY 4.0) |
| `windows` | Windows (the current four-pane mark, no perspective skew) | own geometry |
| `apple` | Apple (also `macos` via alias) | Font Awesome Free brands (CC BY 4.0) |
| `linux` | Tux | Font Awesome Free brands (CC BY 4.0) |
| `steam` | Steam (also `steamos` via alias) | Font Awesome Free brands (CC BY 4.0) |
@@ -17,14 +17,36 @@ Android `ImageVector`s). One file per **icon token** of the host's OS-identity c
| `arch` | Arch Linux | Simple Icons (CC0 1.0) |
| `nixos` | NixOS | Simple Icons (CC0 1.0) |
| `debian` | Debian | Simple Icons (CC0 1.0) |
| `bazzite` | Bazzite | ublue-os/bazzite (Apache-2.0) |
| `cachyos` | CachyOS | Simple Icons (CC0 1.0) |
| `nobara` | Nobara | Simple Icons (CC0 1.0, slug `nobaralinux`) |
Distros with no file here (Bazzite, CachyOS, Nobara, Pop!_OS, Mint, …) are intentional:
the host advertises the full chain (`linux/fedora/bazzite`), and clients walk it
most-specific-first, so they degrade to the family mark and finally to Tux.
The last three are **distro leaves, not families**: a chain walks most-specific-first, so
`linux/fedora/bazzite` would otherwise draw the Fedora mark. They earn their own art because
"a Bazzite box" and "a Fedora box" are different machines to the person reading the card, and
they are what this project's hosts actually run. Every other distro with no file here (Pop!_OS,
Mint, …) still degrades to its family's mark and finally to Tux — that fallback is the design,
not a gap.
All files are monochrome (`fill="currentColor"`), original per-icon viewBoxes preserved.
Licensing: attribution notices live in `LICENSES/` and are folded into
`THIRD-PARTY-NOTICES.txt` by `scripts/gen-third-party-notices.py`. The marks are
trademarks of their respective owners; they are used here nominatively — to *identify*
the operating system a host runs, the standard practice in this ecosystem — and imply no
affiliation or endorsement.
Windows is the one mark drawn here rather than sourced: every icon set that ships a "Windows"
brand glyph still carries the **Windows 8/10 flag with the perspective skew**, which reads as
dated next to the flat four-pane mark Microsoft has used since Windows 11. Four equal squares
(11.377 + 1.246 gap) is the current proportion.
All files are monochrome (`fill="currentColor"`), original per-icon viewBoxes preserved. Because
those viewBoxes are not all square, a client must letterbox rather than stretch — see the aspect
note in `clients/android/.../components/OsIcons.kt`.
## Regenerating the per-client derivatives
`bash scripts/gen-os-icons.sh [token ...]` turns a master into the three baked forms (GTK
symbolic SVG, Windows PNG, Apple template PDF) and prints the path data for the three clients
that inline it (web console, Decky plugin, Android). Adding a **new** token also means adding it
to each client's shipped-token list — the script prints that checklist too.
## Licensing
Attribution notices live in `LICENSES/` and are folded into `THIRD-PARTY-NOTICES.txt` by
`scripts/gen-third-party-notices.py`. The marks are trademarks of their respective owners; they
are used here nominatively — to *identify* the operating system a host runs, the standard
practice in this ecosystem — and imply no affiliation or endorsement.
+2
View File
@@ -0,0 +1,2 @@
<!-- bazzite — the Bazzite "b", from ublue-os/bazzite repo_content/Bazzite.svg (Apache-2.0), lifted out of the badge and normalized to a 24x24 box. See README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M7.178 0h3.589v7.178h7.524c3.153 0 5.709 2.556 5.709 5.709 0 6.138-4.976 11.113-11.113 11.113-3.153 0-5.709-2.556-5.709-5.709V10.766H0v-3.589h7.178zm3.589 10.766v7.524c0 1.171.949 2.12 2.12 2.12 4.156 0 7.524-3.369 7.524-7.524 0-1.171-.949-2.12-2.12-2.12z"/></svg>

After

Width:  |  Height:  |  Size: 523 B

+2
View File
@@ -0,0 +1,2 @@
<!-- cachyos — from Simple Icons (CC0 1.0). See README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.301 2.646 0 11.771l5.541 9.583h11.486l2.904-5.017H8.102l-2.56-4.429L8.067 7.54h6.063l2.83-4.893ZM20.058 4.12a.748.748 0 0 0 0 1.496.748.748 0 0 0 0-1.496m-1.983 4.303a1.45 1.45 0 0 0 0 2.9 1.45 1.45 0 0 0 0-2.9m4.02 3.98a1.904 1.904 0 0 0 0 3.809 1.904 1.904 0 0 0 0-3.81"/></svg>

After

Width:  |  Height:  |  Size: 438 B

+2
View File
@@ -0,0 +1,2 @@
<!-- nobara — from Simple Icons (CC0 1.0), slug "nobaralinux". See README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23.808 11.808v8.281a3.542 3.542 0 0 1-3.542 3.527h-.46a3.543 3.543 0 0 1-3.083-3.513v-7.282l3.543-1.013-3.66-1.045a4.724 4.724 0 0 0-9.33 1.045v2.362a2.362 2.362 0 0 0 2.362 2.362 3.543 3.543 0 0 1 3.543 3.542V24a3.539 3.539 0 0 0-3.542-3.542 3.537 3.537 0 0 0-3.063 1.76 3.54 3.54 0 0 1-2.382 1.398h-.46A3.542 3.542 0 0 1 .192 20.09V3.543a3.542 3.542 0 0 1 6.323-2.194A11.756 11.756 0 0 1 12 0c6.521 0 11.808 5.287 11.808 11.808zm-9.446 0A2.359 2.359 0 0 1 12 14.17a2.362 2.362 0 1 1 2.362-2.362z"/></svg>

After

Width:  |  Height:  |  Size: 681 B

+2 -2
View File
@@ -1,2 +1,2 @@
<!-- windows — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaWindows. See README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor"><path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg>
<!-- windows — the modern (Windows 11) four-pane mark: four equal squares, no perspective skew. Own geometry, see README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h11.377v11.377H0zm12.623 0H24v11.377H12.623zM0 12.623h11.377V24H0zm12.623 0H24V24H12.623z"/></svg>

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 323 B

@@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathParser
import androidx.compose.ui.unit.dp
import io.unom.punktfunk.kit.discovery.osIconTokens
import kotlin.math.max
/**
* The host card's OS marks, resolved from the host's OS-identity chain (mDNS `os` TXT,
@@ -14,18 +15,19 @@ import io.unom.punktfunk.kit.discovery.osIconTokens
* ship, so an unknown distro degrades to its family's mark and finally to Tux; null means
* "no icon", rendering the card exactly as before the field existed.
*
* Path data is vendored from the assets/os-icons masters (Font Awesome Free brands
* CC BY 4.0 + Simple Icons CC0 — provenance in that directory's README); Material ships
* no brand icons. Hand-kept as raw SVG path strings (one line each) rather than
* transcribed ImageVector DSL — [PathParser] builds the vector once, then it's cached.
* Path data is vendored from the assets/os-icons masters (per-mark provenance and licensing
* in that directory's README; `bash scripts/gen-os-icons.sh <token>` prints a master's
* viewport + path ready to paste); Material ships no brand icons. Hand-kept as raw SVG path
* strings (one line each) rather than transcribed ImageVector DSL — [PathParser] builds the
* vector once, then it's cached.
*/
private class OsGlyph(val viewportWidth: Float, val viewportHeight: Float, val d: String)
private val GLYPHS: Map<String, OsGlyph> = mapOf(
"windows" to OsGlyph(
viewportWidth = 448f,
viewportHeight = 512f,
d = "M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z",
viewportWidth = 24f,
viewportHeight = 24f,
d = "M0 0h11.377v11.377H0zm12.623 0H24v11.377H12.623zM0 12.623h11.377V24H0zm12.623 0H24V24H12.623z",
),
"apple" to OsGlyph(
viewportWidth = 384f,
@@ -72,8 +74,28 @@ private val GLYPHS: Map<String, OsGlyph> = mapOf(
viewportHeight = 512f,
d = "M471.08 102.66s-.3 18.3-.3 20.3c-9.1-3-74.4-24.1-135.7-26.3-51.9-1.8-122.8-4.3-223 57.3-19.4 12.4-73.9 46.1-99.6 109.7C7 277-.12 307 7 335.06a111 111 0 0 0 16.5 35.7c17.4 25 46.6 41.6 78.1 44.4 44.4 3.9 78.1-16 90-53.3 8.2-25.8 0-63.6-31.5-82.9-25.6-15.7-53.3-12.1-69.2-1.6-13.9 9.2-21.8 23.5-21.6 39.2.3 27.8 24.3 42.6 41.5 42.6a49 49 0 0 0 15.8-2.7c6.5-1.8 13.3-6.5 13.3-14.9 0-12.1-11.6-14.8-16.8-13.9-2.9.5-4.5 2-11.8 2.4-2-.2-12-3.1-12-14V316c.2-12.3 13.2-18 25.5-16.9 32.3 2.8 47.7 40.7 28.5 65.7-18.3 23.7-76.6 23.2-99.7-20.4-26-49.2 12.7-111.2 87-98.4 33.2 5.7 83.6 35.5 102.4 104.3h45.9c-5.7-17.6-8.9-68.3 42.7-68.3 56.7 0 63.9 39.9 79.8 68.3H460c-12.8-18.3-21.7-38.7-18.9-55.8 5.6-33.8 39.7-18.4 82.4-17.4 66.5.4 102.1-27 103.1-28 3.7-3.1 6.5-15.8 7-17.7 1.3-5.1-3.2-2.4-3.2-2.4-8.7 5.2-30.5 15.2-50.9 15.6-25.3.5-76.2-25.4-81.6-28.2-.3-.4.1 1.2-11-25.5 88.4 58.3 118.3 40.5 145.2 21.7.8-.6 4.3-2.9 3.6-5.7-13.8-48.1-22.4-62.7-34.5-69.6-37-21.6-125-34.7-129.2-35.3.1-.1-.9-.3-.9.7zm60.4 72.8a37.54 37.54 0 0 1 38.9-36.3c33.4 1.2 48.8 42.3 24.4 65.2-24.2 22.7-64.4 4.6-63.3-28.9zm38.6-25.3a26.27 26.27 0 1 0 25.4 27.2 26.19 26.19 0 0 0-25.4-27.2zm4.3 28.8c-15.4 0-15.4-15.6 0-15.6s15.4 15.64 0 15.64z",
),
// The gaming distros get their own mark rather than their family's: "a Bazzite box" and
// "a Fedora box" are different machines to the person reading the card.
"bazzite" to OsGlyph(
viewportWidth = 24f,
viewportHeight = 24f,
d = "M7.178 0h3.589v7.178h7.524c3.153 0 5.709 2.556 5.709 5.709 0 6.138-4.976 11.113-11.113 11.113-3.153 0-5.709-2.556-5.709-5.709V10.766H0v-3.589h7.178zm3.589 10.766v7.524c0 1.171.949 2.12 2.12 2.12 4.156 0 7.524-3.369 7.524-7.524 0-1.171-.949-2.12-2.12-2.12z",
),
"cachyos" to OsGlyph(
viewportWidth = 24f,
viewportHeight = 24f,
d = "M5.301 2.646 0 11.771l5.541 9.583h11.486l2.904-5.017H8.102l-2.56-4.429L8.067 7.54h6.063l2.83-4.893ZM20.058 4.12a.748.748 0 0 0 0 1.496.748.748 0 0 0 0-1.496m-1.983 4.303a1.45 1.45 0 0 0 0 2.9 1.45 1.45 0 0 0 0-2.9m4.02 3.98a1.904 1.904 0 0 0 0 3.809 1.904 1.904 0 0 0 0-3.81",
),
"nobara" to OsGlyph(
viewportWidth = 24f,
viewportHeight = 24f,
d = "M23.808 11.808v8.281a3.542 3.542 0 0 1-3.542 3.527h-.46a3.543 3.543 0 0 1-3.083-3.513v-7.282l3.543-1.013-3.66-1.045a4.724 4.724 0 0 0-9.33 1.045v2.362a2.362 2.362 0 0 0 2.362 2.362 3.543 3.543 0 0 1 3.543 3.542V24a3.539 3.539 0 0 0-3.542-3.542 3.537 3.537 0 0 0-3.063 1.76 3.54 3.54 0 0 1-2.382 1.398h-.46A3.542 3.542 0 0 1 .192 20.09V3.543a3.542 3.542 0 0 1 6.323-2.194A11.756 11.756 0 0 1 12 0c6.521 0 11.808 5.287 11.808 11.808zm-9.446 0A2.359 2.359 0 0 1 12 14.17a2.362 2.362 0 1 1 2.362-2.362z",
),
)
/** Longest edge of a built mark, in dp — the box callers size the [Icon] to. */
private const val GLYPH_DP = 24f
private val built = mutableMapOf<String, ImageVector>()
/** The mark for a chain, or null (no icon). Vectors build lazily and cache per token. */
@@ -82,11 +104,18 @@ fun resolveOsIcon(chain: String): ImageVector? =
GLYPHS[token]?.let { glyph -> built.getOrPut(token) { glyph.build(token) } }
}
private fun OsGlyph.build(token: String): ImageVector =
ImageVector.Builder(
private fun OsGlyph.build(token: String): ImageVector {
// The intrinsic size has to carry the VIEWPORT'S ASPECT RATIO, not a fixed square:
// a VectorPainter maps the viewport onto the default size with independent x and y
// scales, so declaring a 448x512 mark as 24x24 dp stretches it horizontally — which is
// exactly how Tux and the Apple mark used to come out on a phone. Scaling the longest
// edge to GLYPH_DP instead keeps the ratio, and Icon() paints with ContentScale.Fit, so
// the mark letterboxes inside whatever box the caller sized us to.
val longest = max(viewportWidth, viewportHeight)
return ImageVector.Builder(
name = "OsIcon.$token",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
defaultWidth = (GLYPH_DP * viewportWidth / longest).dp,
defaultHeight = (GLYPH_DP * viewportHeight / longest).dp,
viewportWidth = viewportWidth,
viewportHeight = viewportHeight,
).apply {
@@ -96,3 +125,4 @@ private fun OsGlyph.build(token: String): ImageVector =
fill = SolidColor(Color.Black),
)
}.build()
}
@@ -0,0 +1,72 @@
package io.unom.punktfunk
import io.unom.punktfunk.components.resolveOsIcon
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
/**
* Pure JVM test of the host card's OS marks (`components/OsIcons.kt`). Run:
* `./gradlew :app:testDebugUnitTest`.
*
* The aspect assertions are the point: a [androidx.compose.ui.graphics.vector.VectorPainter] maps
* the viewport onto the vector's default size with independent x and y scales, so a mark whose
* default size does not carry its viewport's ratio renders STRETCHED — silently, with no crash and
* no warning. That is exactly how Tux and the Apple mark used to look on a phone.
*/
class OsIconsTest {
/** `ImageVector.name` is "OsIcon.<token>" — the only handle on which mark got resolved. */
private fun markOf(chain: String) = resolveOsIcon(chain)?.name?.removePrefix("OsIcon.")
@Test
fun defaultSizeCarriesTheViewportAspect() {
for (chain in listOf("windows", "linux", "opensuse", "steam", "apple", "bazzite")) {
val v = resolveOsIcon(chain)
assertNotNull("no mark for $chain", v)
v!!
assertEquals(
"$chain default size must keep the viewport ratio",
v.viewportWidth / v.viewportHeight,
v.defaultWidth.value / v.defaultHeight.value,
0.001f,
)
assertEquals(
"$chain longest edge must be the 24dp box",
24f,
maxOf(v.defaultWidth.value, v.defaultHeight.value),
0.001f,
)
}
}
@Test
fun tallMarkIsNarrowerThanItsBox() {
// Tux is 448x512, so a correct build is 21x24dp — 24x24 would be the stretched bug.
val tux = resolveOsIcon("linux")!!
assertEquals(21f, tux.defaultWidth.value, 0.001f)
assertEquals(24f, tux.defaultHeight.value, 0.001f)
}
@Test
fun gamingDistrosResolveToTheirOwnMark() {
// The whole reason these three ship art: without it they'd draw their family's mark.
assertEquals("bazzite", markOf("linux/fedora/bazzite"))
assertEquals("cachyos", markOf("linux/arch/cachyos"))
assertEquals("nobara", markOf("linux/rhel/nobara"))
}
@Test
fun unknownDistroStillDegradesThroughItsFamily() {
assertEquals("fedora", markOf("linux/fedora/somethingnew"))
assertEquals("linux", markOf("linux/frontier/chimera"))
assertEquals("steam", markOf("linux/arch/steamos")) // brand alias
assertEquals("apple", markOf("macos"))
}
@Test
fun noChainMeansNoMark() {
assertNull(resolveOsIcon(""))
assertNull(resolveOsIcon("!!!"))
}
}
+4 -3
View File
@@ -34,9 +34,10 @@ let package = Package(
// Geist (SIL OFL 1.1) the brand typeface, shared with punktfunk-website.
// Registered with Core Text at first use; see BrandFont.swift.
.copy("Resources/Fonts"),
// The host cards' OS marks (template vector imagesets derived from the
// assets/os-icons masters FA brands CC BY 4.0 + Simple Icons CC0, see that
// README). `.process` compiles the catalog; loaded via OsIcon.swift.
// The host cards' OS marks (template vector imagesets generated from the
// assets/os-icons masters by scripts/gen-os-icons.sh per-mark provenance and
// licensing in that README). `.process` compiles the catalog; loaded via
// OsIcon.swift.
.process("Resources/OsIcons.xcassets"),
],
linkerSettings: [
@@ -1,17 +1,19 @@
// The host cards' OS marks: template vector imagesets in Resources/OsIcons.xcassets
// (derived from the repo's assets/os-icons masters Font Awesome Free brands CC BY 4.0 +
// Simple Icons CC0; provenance in that directory's README), resolved from the host's
// (generated from the repo's assets/os-icons masters by scripts/gen-os-icons.sh
// per-mark provenance and licensing in that directory's README), resolved from the host's
// OS-identity chain via PunktfunkShared's `osIconTokens` walk. Template rendering means
// they tint with `foregroundStyle` like an SF Symbol.
import PunktfunkShared
import SwiftUI
/// The icon tokens this client ships art for. A distro without its own mark (Bazzite,
/// CachyOS, ...) degrades to its family's and finally to Tux via the chain walk.
/// The icon tokens this client ships art for: the families a chain can land on, plus the
/// gaming distros that earn their own mark because "a Bazzite box" and "a Fedora box" are
/// different machines to the person reading the card. A distro with no mark of its own
/// still degrades to its family's and finally to Tux via the chain walk.
private let osIconTokensShipped: Set<String> = [
"windows", "apple", "linux", "steam", "ubuntu", "fedora", "arch", "debian", "nixos",
"opensuse",
"opensuse", "bazzite", "cachyos", "nobara",
]
/// The mark for an OS-identity chain (`linux/fedora/bazzite`, ...), or nil no view at
@@ -0,0 +1,10 @@
{
"images" : [
{ "filename" : "bazzite.pdf", "idiom" : "universal" }
],
"info" : { "author" : "xcode", "version" : 1 },
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,10 @@
{
"images" : [
{ "filename" : "cachyos.pdf", "idiom" : "universal" }
],
"info" : { "author" : "xcode", "version" : 1 },
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,10 @@
{
"images" : [
{ "filename" : "nobara.pdf", "idiom" : "universal" }
],
"info" : { "author" : "xcode", "version" : 1 },
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
+89 -33
View File
@@ -1,40 +1,82 @@
// The host row's OS mark, resolved from the host's OS-identity chain (mDNS `os` TXT /
// `--list-hosts` `os`, e.g. "linux/fedora/bazzite"): walk the chain most-specific-first and
// take the first token react-icons has a brand mark for, so an unknown distro degrades to its
// family's mark and finally to Tux. Mirrors pf-client-core's `os_icon_tokens` (aliases
// macos→apple, steamos→steam); null when the chain is absent or entirely unknown — the row
// then renders exactly as it did before the field existed.
// take the first token we ship a mark for, so an unknown distro degrades to its family's
// mark and finally to Tux. Mirrors pf-client-core's `os_icon_tokens` (aliases macos→apple,
// steamos→steam); null when the chain is absent or entirely unknown — the row then renders
// exactly as it did before the field existed.
//
// Path data is transcribed from the assets/os-icons masters rather than pulled from
// react-icons: that package's Font Awesome 5 brand set still carries the perspective-skewed
// Windows flag, and it has no Bazzite or CachyOS mark at all. `bash scripts/gen-os-icons.sh
// <token>` prints a master's viewBox + path ready to paste here.
import { FC } from "react";
import {
FaApple,
FaFedora,
FaLinux,
FaSteam,
FaSuse,
FaUbuntu,
FaWindows,
} from "react-icons/fa";
import { SiArchlinux, SiDebian, SiNixos } from "react-icons/si";
import { IconType } from "react-icons";
const OS_ICONS: Record<string, IconType> = {
windows: FaWindows,
apple: FaApple,
macos: FaApple,
linux: FaLinux,
steam: FaSteam,
steamos: FaSteam,
ubuntu: FaUbuntu,
fedora: FaFedora,
opensuse: FaSuse,
arch: SiArchlinux,
debian: SiDebian,
nixos: SiNixos,
/** One monochrome brand mark: original per-icon viewBox, drawn in currentColor. */
const OS_ICONS: Record<string, { viewBox: string; d: string }> = {
windows: {
viewBox: "0 0 24 24",
d: "M0 0h11.377v11.377H0zm12.623 0H24v11.377H12.623zM0 12.623h11.377V24H0zm12.623 0H24V24H12.623z",
},
apple: {
viewBox: "0 0 384 512",
d: "M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z",
},
linux: {
viewBox: "0 0 448 512",
d: "M220.8 123.3c1 .5 1.8 1.7 3 1.7 1.1 0 2.8-.4 2.9-1.5.2-1.4-1.9-2.3-3.2-2.9-1.7-.7-3.9-1-5.5-.1-.4.2-.8.7-.6 1.1.3 1.3 2.3 1.1 3.4 1.7zm-21.9 1.7c1.2 0 2-1.2 3-1.7 1.1-.6 3.1-.4 3.5-1.6.2-.4-.2-.9-.6-1.1-1.6-.9-3.8-.6-5.5.1-1.3.6-3.4 1.5-3.2 2.9.1 1 1.8 1.5 2.8 1.4zM420 403.8c-3.6-4-5.3-11.6-7.2-19.7-1.8-8.1-3.9-16.8-10.5-22.4-1.3-1.1-2.6-2.1-4-2.9-1.3-.8-2.7-1.5-4.1-2 9.2-27.3 5.6-54.5-3.7-79.1-11.4-30.1-31.3-56.4-46.5-74.4-17.1-21.5-33.7-41.9-33.4-72C311.1 85.4 315.7.1 234.8 0 132.4-.2 158 103.4 156.9 135.2c-1.7 23.4-6.4 41.8-22.5 64.7-18.9 22.5-45.5 58.8-58.1 96.7-6 17.9-8.8 36.1-6.2 53.3-6.5 5.8-11.4 14.7-16.6 20.2-4.2 4.3-10.3 5.9-17 8.3s-14 6-18.5 14.5c-2.1 3.9-2.8 8.1-2.8 12.4 0 3.9.6 7.9 1.2 11.8 1.2 8.1 2.5 15.7.8 20.8-5.2 14.4-5.9 24.4-2.2 31.7 3.8 7.3 11.4 10.5 20.1 12.3 17.3 3.6 40.8 2.7 59.3 12.5 19.8 10.4 39.9 14.1 55.9 10.4 11.6-2.6 21.1-9.6 25.9-20.2 12.5-.1 26.3-5.4 48.3-6.6 14.9-1.2 33.6 5.3 55.1 4.1.6 2.3 1.4 4.6 2.5 6.7v.1c8.3 16.7 23.8 24.3 40.3 23 16.6-1.3 34.1-11 48.3-27.9 13.6-16.4 36-23.2 50.9-32.2 7.4-4.5 13.4-10.1 13.9-18.3.4-8.2-4.4-17.3-15.5-29.7zM223.7 87.3c9.8-22.2 34.2-21.8 44-.4 6.5 14.2 3.6 30.9-4.3 40.4-1.6-.8-5.9-2.6-12.6-4.9 1.1-1.2 3.1-2.7 3.9-4.6 4.8-11.8-.2-27-9.1-27.3-7.3-.5-13.9 10.8-11.8 23-4.1-2-9.4-3.5-13-4.4-1-6.9-.3-14.6 2.9-21.8zM183 75.8c10.1 0 20.8 14.2 19.1 33.5-3.5 1-7.1 2.5-10.2 4.6 1.2-8.9-3.3-20.1-9.6-19.6-8.4.7-9.8 21.2-1.8 28.1 1 .8 1.9-.2-5.9 5.5-15.6-14.6-10.5-52.1 8.4-52.1zm-13.6 60.7c6.2-4.6 13.6-10 14.1-10.5 4.7-4.4 13.5-14.2 27.9-14.2 7.1 0 15.6 2.3 25.9 8.9 6.3 4.1 11.3 4.4 22.6 9.3 8.4 3.5 13.7 9.7 10.5 18.2-2.6 7.1-11 14.4-22.7 18.1-11.1 3.6-19.8 16-38.2 14.9-3.9-.2-7-1-9.6-2.1-8-3.5-12.2-10.4-20-15-8.6-4.8-13.2-10.4-14.7-15.3-1.4-4.9 0-9 4.2-12.3zm3.3 334c-2.7 35.1-43.9 34.4-75.3 18-29.9-15.8-68.6-6.5-76.5-21.9-2.4-4.7-2.4-12.7 2.6-26.4v-.2c2.4-7.6.6-16-.6-23.9-1.2-7.8-1.8-15 .9-20 3.5-6.7 8.5-9.1 14.8-11.3 10.3-3.7 11.8-3.4 19.6-9.9 5.5-5.7 9.5-12.9 14.3-18 5.1-5.5 10-8.1 17.7-6.9 8.1 1.2 15.1 6.8 21.9 16l19.6 35.6c9.5 19.9 43.1 48.4 41 68.9zm-1.4-25.9c-4.1-6.6-9.6-13.6-14.4-19.6 7.1 0 14.2-2.2 16.7-8.9 2.3-6.2 0-14.9-7.4-24.9-13.5-18.2-38.3-32.5-38.3-32.5-13.5-8.4-21.1-18.7-24.6-29.9s-3-23.3-.3-35.2c5.2-22.9 18.6-45.2 27.2-59.2 2.3-1.7.8 3.2-8.7 20.8-8.5 16.1-24.4 53.3-2.6 82.4.6-20.7 5.5-41.8 13.8-61.5 12-27.4 37.3-74.9 39.3-112.7 1.1.8 4.6 3.2 6.2 4.1 4.6 2.7 8.1 6.7 12.6 10.3 12.4 10 28.5 9.2 42.4 1.2 6.2-3.5 11.2-7.5 15.9-9 9.9-3.1 17.8-8.6 22.3-15 7.7 30.4 25.7 74.3 37.2 95.7 6.1 11.4 18.3 35.5 23.6 64.6 3.3-.1 7 .4 10.9 1.4 13.8-35.7-11.7-74.2-23.3-84.9-4.7-4.6-4.9-6.6-2.6-6.5 12.6 11.2 29.2 33.7 35.2 59 2.8 11.6 3.3 23.7.4 35.7 16.4 6.8 35.9 17.9 30.7 34.8-2.2-.1-3.2 0-4.2 0 3.2-10.1-3.9-17.6-22.8-26.1-19.6-8.6-36-8.6-38.3 12.5-12.1 4.2-18.3 14.7-21.4 27.3-2.8 11.2-3.6 24.7-4.4 39.9-.5 7.7-3.6 18-6.8 29-32.1 22.9-76.7 32.9-114.3 7.2zm257.4-11.5c-.9 16.8-41.2 19.9-63.2 46.5-13.2 15.7-29.4 24.4-43.6 25.5s-26.5-4.8-33.7-19.3c-4.7-11.1-2.4-23.1 1.1-36.3 3.7-14.2 9.2-28.8 9.9-40.6.8-15.2 1.7-28.5 4.2-38.7 2.6-10.3 6.6-17.2 13.7-21.1.3-.2.7-.3 1-.5.8 13.2 7.3 26.6 18.8 29.5 12.6 3.3 30.7-7.5 38.4-16.3 9-.3 15.7-.9 22.6 5.1 9.9 8.5 7.1 30.3 17.1 41.6 10.6 11.6 14 19.5 13.7 24.6zM173.3 148.7c2 1.9 4.7 4.5 8 7.1 6.6 5.2 15.8 10.6 27.3 10.6 11.6 0 22.5-5.9 31.8-10.8 4.9-2.6 10.9-7 14.8-10.4s5.9-6.3 3.1-6.6-2.6 2.6-6 5.1c-4.4 3.2-9.7 7.4-13.9 9.8-7.4 4.2-19.5 10.2-29.9 10.2s-18.7-4.8-24.9-9.7c-3.1-2.5-5.7-5-7.7-6.9-1.5-1.4-1.9-4.6-4.3-4.9-1.4-.1-1.8 3.7 1.7 6.5z",
},
steam: {
viewBox: "0 0 496 512",
d: "M496 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5v1.2L176.6 279c-15.5-.9-30.7 3.4-43.5 12.1L0 236.1C10.2 108.4 117.1 8 247.6 8 384.8 8 496 119 496 256zM155.7 384.3l-30.5-12.6a52.79 52.79 0 0 0 27.2 25.8c26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3.1-40.3-5.4-13-15.5-23.2-28.5-28.6-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zm173.8-129.9c-34.4 0-62.4-28-62.4-62.3s28-62.3 62.4-62.3 62.4 28 62.4 62.3-27.9 62.3-62.4 62.3zm.1-15.6c25.9 0 46.9-21 46.9-46.8 0-25.9-21-46.8-46.9-46.8s-46.9 21-46.9 46.8c.1 25.8 21.1 46.8 46.9 46.8z",
},
ubuntu: {
viewBox: "0 0 496 512",
d: "M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm52.7 93c8.8-15.2 28.3-20.5 43.5-11.7 15.3 8.8 20.5 28.3 11.7 43.6-8.8 15.2-28.3 20.5-43.5 11.7-15.3-8.9-20.5-28.4-11.7-43.6zM87.4 287.9c-17.6 0-31.9-14.3-31.9-31.9 0-17.6 14.3-31.9 31.9-31.9 17.6 0 31.9 14.3 31.9 31.9 0 17.6-14.3 31.9-31.9 31.9zm28.1 3.1c22.3-17.9 22.4-51.9 0-69.9 8.6-32.8 29.1-60.7 56.5-79.1l23.7 39.6c-51.5 36.3-51.5 112.5 0 148.8L172 370c-27.4-18.3-47.8-46.3-56.5-79zm228.7 131.7c-15.3 8.8-34.7 3.6-43.5-11.7-8.8-15.3-3.6-34.8 11.7-43.6 15.2-8.8 34.7-3.6 43.5 11.7 8.8 15.3 3.6 34.8-11.7 43.6zm.3-69.5c-26.7-10.3-56.1 6.6-60.5 35-5.2 1.4-48.9 14.3-96.7-9.4l22.5-40.3c57 26.5 123.4-11.7 128.9-74.4l46.1.7c-2.3 34.5-17.3 65.5-40.3 88.4zm-5.9-105.3c-5.4-62-71.3-101.2-128.9-74.4l-22.5-40.3c47.9-23.7 91.5-10.8 96.7-9.4 4.4 28.3 33.8 45.3 60.5 35 23.1 22.9 38 53.9 40.2 88.5l-46 .6z",
},
fedora: {
viewBox: "0 0 448 512",
d: "M225 32C101.3 31.7.8 131.7.4 255.4L0 425.7a53.6 53.6 0 0 0 53.6 53.9l170.2.4c123.7.3 224.3-99.7 224.6-223.4S348.7 32.3 225 32zm169.8 157.2L333 126.6c2.3-4.7 3.8-9.2 3.8-14.3v-1.6l55.2 56.1a101 101 0 0 1 2.8 22.4zM331 94.3a106.06 106.06 0 0 1 58.5 63.8l-54.3-54.6a26.48 26.48 0 0 0-4.2-9.2zM118.1 247.2a49.66 49.66 0 0 0-7.7 11.4l-8.5-8.5a85.78 85.78 0 0 1 16.2-2.9zM97 251.4l11.8 11.9-.9 8a34.74 34.74 0 0 0 2.4 12.5l-27-27.2a80.6 80.6 0 0 1 13.7-5.2zm-18.2 7.4l38.2 38.4a53.17 53.17 0 0 0-14.1 4.7L67.6 266a107 107 0 0 1 11.2-7.2zm-15.2 9.8l35.3 35.5a67.25 67.25 0 0 0-10.5 8.5L53.5 278a64.33 64.33 0 0 1 10.1-9.4zm-13.3 12.3l34.9 35a56.84 56.84 0 0 0-7.7 11.4l-35.8-35.9c2.8-3.8 5.7-7.2 8.6-10.5zm-11 14.3l36.4 36.6a48.29 48.29 0 0 0-3.6 15.2l-39.5-39.8a99.81 99.81 0 0 1 6.7-12zm-8.8 16.3l41.3 41.8a63.47 63.47 0 0 0 6.7 26.2L25.8 326c1.4-4.9 2.9-9.6 4.7-14.5zm-7.9 43l61.9 62.2a31.24 31.24 0 0 0-3.6 14.3v1.1l-55.4-55.7a88.27 88.27 0 0 1-2.9-21.9zm5.3 30.7l54.3 54.6a28.44 28.44 0 0 0 4.2 9.2 106.32 106.32 0 0 1-58.5-63.8zm-5.3-37a80.69 80.69 0 0 1 2.1-17l72.2 72.5a37.59 37.59 0 0 0-9.9 8.7zm253.3-51.8l-42.6-.1-.1 56c-.2 69.3-64.4 115.8-125.7 102.9-5.7 0-19.9-8.7-19.9-24.2a24.89 24.89 0 0 1 24.5-24.6c6.3 0 6.3 1.6 15.7 1.6a55.91 55.91 0 0 0 56.1-55.9l.1-47c0-4.5-4.5-9-8.9-9l-33.6-.1c-32.6-.1-32.5-49.4.1-49.3l42.6.1.1-56a105.18 105.18 0 0 1 105.6-105 86.35 86.35 0 0 1 20.2 2.3c11.2 1.8 19.9 11.9 19.9 24 0 15.5-14.9 27.8-30.3 23.9-27.4-5.9-65.9 14.4-66 54.9l-.1 47a8.94 8.94 0 0 0 8.9 9l33.6.1c32.5.2 32.4 49.5-.2 49.4zm23.5-.3a35.58 35.58 0 0 0 7.6-11.4l8.5 8.5a102 102 0 0 1-16.1 2.9zm21-4.2L308.6 280l.9-8.1a34.74 34.74 0 0 0-2.4-12.5l27 27.2a74.89 74.89 0 0 1-13.7 5.3zm18-7.4l-38-38.4c4.9-1.1 9.6-2.4 13.7-4.7l36.2 35.9c-3.8 2.5-7.9 5-11.9 7.2zm15.5-9.8l-35.3-35.5a61.06 61.06 0 0 0 10.5-8.5l34.9 35a124.56 124.56 0 0 1-10.1 9zm13.2-12.3l-34.9-35a63.18 63.18 0 0 0 7.7-11.4l35.8 35.9a130.28 130.28 0 0 1-8.6 10.5zm11-14.3l-36.4-36.6a48.29 48.29 0 0 0 3.6-15.2l39.5 39.8a87.72 87.72 0 0 1-6.7 12zm13.5-30.9a140.63 140.63 0 0 1-4.7 14.3L345.6 190a58.19 58.19 0 0 0-7.1-26.2zm1-5.6l-71.9-72.1a32 32 0 0 0 9.9-9.2l64.3 64.7a90.93 90.93 0 0 1-2.3 16.6z",
},
arch: {
viewBox: "0 0 24 24",
d: "M11.39.605C10.376 3.092 9.764 4.72 8.635 7.132c.693.734 1.543 1.589 2.923 2.554-1.484-.61-2.496-1.224-3.252-1.86C6.86 10.842 4.596 15.138 0 23.395c3.612-2.085 6.412-3.37 9.021-3.862a6.61 6.61 0 01-.171-1.547l.003-.115c.058-2.315 1.261-4.095 2.687-3.973 1.426.12 2.534 2.096 2.478 4.409a6.52 6.52 0 01-.146 1.243c2.58.505 5.352 1.787 8.914 3.844-.702-1.293-1.33-2.459-1.929-3.57-.943-.73-1.926-1.682-3.933-2.713 1.38.359 2.367.772 3.137 1.234-6.09-11.334-6.582-12.84-8.67-17.74zM22.898 21.36v-.623h-.234v-.084h.562v.084h-.234v.623h.331v-.707h.142l.167.5.034.107a2.26 2.26 0 01.038-.114l.17-.493H24v.707h-.091v-.593l-.206.593h-.084l-.205-.602v.602h-.091",
},
debian: {
viewBox: "0 0 24 24",
d: "M13.88 12.685c-.4 0 .08.2.601.28.14-.1.27-.22.39-.33a3.001 3.001 0 01-.99.05m2.14-.53c.23-.33.4-.69.47-1.06-.06.27-.2.5-.33.73-.75.47-.07-.27 0-.56-.8 1.01-.11.6-.14.89m.781-2.05c.05-.721-.14-.501-.2-.221.07.04.13.5.2.22M12.38.31c.2.04.45.07.42.12.23-.05.28-.1-.43-.12m.43.12l-.15.03.14-.01V.43m6.633 9.944c.02.64-.2.95-.38 1.5l-.35.181c-.28.54.03.35-.17.78-.44.39-1.34 1.22-1.62 1.301-.201 0 .14-.25.19-.34-.591.4-.481.6-1.371.85l-.03-.06c-2.221 1.04-5.303-1.02-5.253-3.842-.03.17-.07.13-.12.2a3.551 3.552 0 012.001-3.501 3.361 3.362 0 013.732.48 3.341 3.342 0 00-2.721-1.3c-1.18.01-2.281.76-2.651 1.57-.6.38-.67 1.47-.93 1.661-.361 2.601.66 3.722 2.38 5.042.27.19.08.21.12.35a4.702 4.702 0 01-1.53-1.16c.23.33.47.66.8.91-.55-.18-1.27-1.3-1.48-1.35.93 1.66 3.78 2.921 5.261 2.3a6.203 6.203 0 01-2.33-.28c-.33-.16-.77-.51-.7-.57a5.802 5.803 0 005.902-.84c.44-.35.93-.94 1.07-.95-.2.32.04.16-.12.44.44-.72-.2-.3.46-1.24l.24.33c-.09-.6.74-1.321.66-2.262.19-.3.2.3 0 .97.29-.74.08-.85.15-1.46.08.2.18.42.23.63-.18-.7.2-1.2.28-1.6-.09-.05-.28.3-.32-.53 0-.37.1-.2.14-.28-.08-.05-.26-.32-.38-.861.08-.13.22.33.34.34-.08-.42-.2-.75-.2-1.08-.34-.68-.12.1-.4-.3-.34-1.091.3-.25.34-.74.54.77.84 1.96.981 2.46-.1-.6-.28-1.2-.49-1.76.16.07-.26-1.241.21-.37A7.823 7.824 0 0017.702 1.6c.18.17.42.39.33.42-.75-.45-.62-.48-.73-.67-.61-.25-.65.02-1.06 0C15.082.73 14.862.8 13.8.4l.05.23c-.77-.25-.9.1-1.73 0-.05-.04.27-.14.53-.18-.741.1-.701-.14-1.431.03.17-.13.36-.21.55-.32-.6.04-1.44.35-1.18.07C9.6.68 7.847 1.3 6.867 2.22L6.838 2c-.45.54-1.96 1.611-2.08 2.311l-.131.03c-.23.4-.38.85-.57 1.261-.3.52-.45.2-.4.28-.6 1.22-.9 2.251-1.16 3.102.18.27 0 1.65.07 2.76-.3 5.463 3.84 10.776 8.363 12.006.67.23 1.65.23 2.49.25-.99-.28-1.12-.15-2.08-.49-.7-.32-.85-.7-1.34-1.13l.2.35c-.971-.34-.57-.42-1.361-.67l.21-.27c-.31-.03-.83-.53-.97-.81l-.34.01c-.41-.501-.63-.871-.61-1.161l-.111.2c-.13-.21-1.52-1.901-.8-1.511-.13-.12-.31-.2-.5-.55l.14-.17c-.35-.44-.64-1.02-.62-1.2.2.24.32.3.45.33-.88-2.172-.93-.12-1.601-2.202l.15-.02c-.1-.16-.18-.34-.26-.51l.06-.6c-.63-.74-.18-3.102-.09-4.402.07-.54.53-1.1.88-1.981l-.21-.04c.4-.71 2.341-2.872 3.241-2.761.43-.55-.09 0-.18-.14.96-.991 1.26-.7 1.901-.88.7-.401-.6.16-.27-.151 1.2-.3.85-.7 2.421-.85.16.1-.39.14-.52.26 1-.49 3.151-.37 4.562.27 1.63.77 3.461 3.011 3.531 5.132l.08.02c-.04.85.13 1.821-.17 2.711l.2-.42M9.54 13.236l-.05.28c.26.35.47.73.8 1.01-.24-.47-.42-.66-.75-1.3m.62-.02c-.14-.15-.22-.34-.31-.52.08.32.26.6.43.88l-.12-.36m10.945-2.382l-.07.15c-.1.76-.34 1.511-.69 2.212.4-.73.65-1.541.75-2.362M12.45.12c.27-.1.66-.05.95-.12-.37.03-.74.05-1.1.1l.15.02M3.006 5.142c.07.57-.43.8.11.42.3-.66-.11-.18-.1-.42m-.64 2.661c.12-.39.15-.62.2-.84-.35.44-.17.53-.2.83",
},
nixos: {
viewBox: "0 0 24 24",
d: "M7.352 1.592l-1.364.002L5.32 2.75l1.557 2.713-3.137-.008-1.32 2.34H14.11l-1.353-2.332-3.192-.006-2.214-3.865zm6.175 0l-2.687.025 5.846 10.127 1.341-2.34-1.59-2.765 2.24-3.85-.683-1.182h-1.336l-1.57 2.705-1.56-2.72zm6.887 4.195l-5.846 10.125 2.696-.008 1.601-2.76 4.453.016.682-1.183-.666-1.157-3.13-.008L21.778 8.1l-1.365-2.313zM9.432 8.086l-2.696.008-1.601 2.76-4.453-.016L0 12.02l.666 1.157 3.13.008-1.575 2.71 1.365 2.315L9.432 8.086zM7.33 12.25l-.006.01-.002-.004-1.342 2.34 1.59 2.765-2.24 3.85.684 1.182H7.35l.004-.006h.001l1.567-2.698 1.558 2.72 2.688-.026-.004-.006h.01L7.33 12.25zm2.55 3.93l1.354 2.332 3.192.006 2.215 3.865 1.363-.002.668-1.156-1.557-2.713 3.137.008 1.32-2.34H9.881Z",
},
opensuse: {
viewBox: "0 0 640 512",
d: "M471.08 102.66s-.3 18.3-.3 20.3c-9.1-3-74.4-24.1-135.7-26.3-51.9-1.8-122.8-4.3-223 57.3-19.4 12.4-73.9 46.1-99.6 109.7C7 277-.12 307 7 335.06a111 111 0 0 0 16.5 35.7c17.4 25 46.6 41.6 78.1 44.4 44.4 3.9 78.1-16 90-53.3 8.2-25.8 0-63.6-31.5-82.9-25.6-15.7-53.3-12.1-69.2-1.6-13.9 9.2-21.8 23.5-21.6 39.2.3 27.8 24.3 42.6 41.5 42.6a49 49 0 0 0 15.8-2.7c6.5-1.8 13.3-6.5 13.3-14.9 0-12.1-11.6-14.8-16.8-13.9-2.9.5-4.5 2-11.8 2.4-2-.2-12-3.1-12-14V316c.2-12.3 13.2-18 25.5-16.9 32.3 2.8 47.7 40.7 28.5 65.7-18.3 23.7-76.6 23.2-99.7-20.4-26-49.2 12.7-111.2 87-98.4 33.2 5.7 83.6 35.5 102.4 104.3h45.9c-5.7-17.6-8.9-68.3 42.7-68.3 56.7 0 63.9 39.9 79.8 68.3H460c-12.8-18.3-21.7-38.7-18.9-55.8 5.6-33.8 39.7-18.4 82.4-17.4 66.5.4 102.1-27 103.1-28 3.7-3.1 6.5-15.8 7-17.7 1.3-5.1-3.2-2.4-3.2-2.4-8.7 5.2-30.5 15.2-50.9 15.6-25.3.5-76.2-25.4-81.6-28.2-.3-.4.1 1.2-11-25.5 88.4 58.3 118.3 40.5 145.2 21.7.8-.6 4.3-2.9 3.6-5.7-13.8-48.1-22.4-62.7-34.5-69.6-37-21.6-125-34.7-129.2-35.3.1-.1-.9-.3-.9.7zm60.4 72.8a37.54 37.54 0 0 1 38.9-36.3c33.4 1.2 48.8 42.3 24.4 65.2-24.2 22.7-64.4 4.6-63.3-28.9zm38.6-25.3a26.27 26.27 0 1 0 25.4 27.2 26.19 26.19 0 0 0-25.4-27.2zm4.3 28.8c-15.4 0-15.4-15.6 0-15.6s15.4 15.64 0 15.64z",
},
// The gaming distros get their own mark rather than their family's: on a Deck, "a Bazzite
// box" and "a Fedora box" are different machines to the person reading the row.
bazzite: {
viewBox: "0 0 24 24",
d: "M7.178 0h3.589v7.178h7.524c3.153 0 5.709 2.556 5.709 5.709 0 6.138-4.976 11.113-11.113 11.113-3.153 0-5.709-2.556-5.709-5.709V10.766H0v-3.589h7.178zm3.589 10.766v7.524c0 1.171.949 2.12 2.12 2.12 4.156 0 7.524-3.369 7.524-7.524 0-1.171-.949-2.12-2.12-2.12z",
},
cachyos: {
viewBox: "0 0 24 24",
d: "M5.301 2.646 0 11.771l5.541 9.583h11.486l2.904-5.017H8.102l-2.56-4.429L8.067 7.54h6.063l2.83-4.893ZM20.058 4.12a.748.748 0 0 0 0 1.496.748.748 0 0 0 0-1.496m-1.983 4.303a1.45 1.45 0 0 0 0 2.9 1.45 1.45 0 0 0 0-2.9m4.02 3.98a1.904 1.904 0 0 0 0 3.809 1.904 1.904 0 0 0 0-3.81",
},
nobara: {
viewBox: "0 0 24 24",
d: "M23.808 11.808v8.281a3.542 3.542 0 0 1-3.542 3.527h-.46a3.543 3.543 0 0 1-3.083-3.513v-7.282l3.543-1.013-3.66-1.045a4.724 4.724 0 0 0-9.33 1.045v2.362a2.362 2.362 0 0 0 2.362 2.362 3.543 3.543 0 0 1 3.543 3.542V24a3.539 3.539 0 0 0-3.542-3.542 3.537 3.537 0 0 0-3.063 1.76 3.54 3.54 0 0 1-2.382 1.398h-.46A3.542 3.542 0 0 1 .192 20.09V3.543a3.542 3.542 0 0 1 6.323-2.194A11.756 11.756 0 0 1 12 0c6.521 0 11.808 5.287 11.808 11.808zm-9.446 0A2.359 2.359 0 0 1 12 14.17a2.362 2.362 0 1 1 2.362-2.362z",
},
};
export function resolveOsIcon(os: string | undefined): IconType | null {
for (const token of (os ?? "").toLowerCase().split("/").reverse()) {
const icon = OS_ICONS[token];
/** `macos` renders the apple mark, `steamos` the steam mark — same aliases as every client. */
const ALIASES: Record<string, string> = { macos: "apple", steamos: "steam" };
export function resolveOsIcon(
os: string | undefined,
): { viewBox: string; d: string } | null {
for (const raw of (os ?? "").toLowerCase().split("/").reverse()) {
const icon = OS_ICONS[ALIASES[raw] ?? raw];
if (icon) {
return icon;
}
@@ -44,6 +86,20 @@ export function resolveOsIcon(os: string | undefined): IconType | null {
/** The mark itself, or nothing — sized/colored by the surrounding text like the lock glyph. */
export const OsMark: FC<{ os?: string }> = ({ os }) => {
const Icon = resolveOsIcon(os);
return Icon ? <Icon /> : null;
const icon = resolveOsIcon(os);
if (!icon) return null;
// 1em square with the viewBox letterboxed inside it: the same box react-icons drew, so a
// non-square mark keeps its aspect ratio instead of being stretched to fill.
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={icon.viewBox}
height="1em"
width="1em"
fill="currentColor"
aria-hidden
>
<path d={icon.d} />
</svg>
);
};
@@ -0,0 +1,2 @@
<!-- bazzite — the Bazzite "b", from ublue-os/bazzite repo_content/Bazzite.svg (Apache-2.0), lifted out of the badge and normalized to a 24x24 box. See README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M7.178 0h3.589v7.178h7.524c3.153 0 5.709 2.556 5.709 5.709 0 6.138-4.976 11.113-11.113 11.113-3.153 0-5.709-2.556-5.709-5.709V10.766H0v-3.589h7.178zm3.589 10.766v7.524c0 1.171.949 2.12 2.12 2.12 4.156 0 7.524-3.369 7.524-7.524 0-1.171-.949-2.12-2.12-2.12z"/></svg>

After

Width:  |  Height:  |  Size: 518 B

@@ -0,0 +1,2 @@
<!-- cachyos — from Simple Icons (CC0 1.0). See README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M5.301 2.646 0 11.771l5.541 9.583h11.486l2.904-5.017H8.102l-2.56-4.429L8.067 7.54h6.063l2.83-4.893ZM20.058 4.12a.748.748 0 0 0 0 1.496.748.748 0 0 0 0-1.496m-1.983 4.303a1.45 1.45 0 0 0 0 2.9 1.45 1.45 0 0 0 0-2.9m4.02 3.98a1.904 1.904 0 0 0 0 3.809 1.904 1.904 0 0 0 0-3.81"/></svg>

After

Width:  |  Height:  |  Size: 433 B

@@ -0,0 +1,2 @@
<!-- nobara — from Simple Icons (CC0 1.0), slug "nobaralinux". See README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M23.808 11.808v8.281a3.542 3.542 0 0 1-3.542 3.527h-.46a3.543 3.543 0 0 1-3.083-3.513v-7.282l3.543-1.013-3.66-1.045a4.724 4.724 0 0 0-9.33 1.045v2.362a2.362 2.362 0 0 0 2.362 2.362 3.543 3.543 0 0 1 3.543 3.542V24a3.539 3.539 0 0 0-3.542-3.542 3.537 3.537 0 0 0-3.063 1.76 3.54 3.54 0 0 1-2.382 1.398h-.46A3.542 3.542 0 0 1 .192 20.09V3.543a3.542 3.542 0 0 1 6.323-2.194A11.756 11.756 0 0 1 12 0c6.521 0 11.808 5.287 11.808 11.808zm-9.446 0A2.359 2.359 0 0 1 12 14.17a2.362 2.362 0 1 1 2.362-2.362z"/></svg>

After

Width:  |  Height:  |  Size: 676 B

@@ -1,2 +1,2 @@
<!-- windows — from Font Awesome Free 5 brands (CC BY 4.0), via react-icons FaWindows. See README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#000000"><path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg>
<!-- windows — the modern (Windows 11) four-pane mark: four equal squares, no perspective skew. Own geometry, see README.md. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M0 0h11.377v11.377H0zm12.623 0H24v11.377H12.623zM0 12.623h11.377V24H0zm12.623 0H24V24H12.623z"/></svg>

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 318 B

@@ -14,5 +14,8 @@
<file>icons/scalable/actions/pf-os-debian-symbolic.svg</file>
<file>icons/scalable/actions/pf-os-nixos-symbolic.svg</file>
<file>icons/scalable/actions/pf-os-opensuse-symbolic.svg</file>
<file>icons/scalable/actions/pf-os-bazzite-symbolic.svg</file>
<file>icons/scalable/actions/pf-os-cachyos-symbolic.svg</file>
<file>icons/scalable/actions/pf-os-nobara-symbolic.svg</file>
</gresource>
</gresources>
+6 -3
View File
@@ -583,10 +583,13 @@ const PROBE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(12);
/// at a glance is the whole reason the chip exists. No colour set keeps the neutral pill, so
/// the palette stays opt-in.
/// The OS-icon tokens this shell ships symbolic art for (`data/icons/.../pf-os-<t>-symbolic.svg`,
/// embedded via gresource). Chains walk most-specific-first, so a distro without its own mark
/// (Bazzite, CachyOS, ...) lands on its family's and finally on plain Tux.
/// embedded via gresource): the families a chain can land on, plus the gaming distros that earn
/// their own mark because "a Bazzite box" and "a Fedora box" are different machines to the person
/// reading the card. Chains walk most-specific-first, so a distro without a mark of its own still
/// lands on its family's and finally on plain Tux.
const OS_ICON_TOKENS: &[&str] = &[
"windows", "apple", "linux", "steam", "ubuntu", "fedora", "arch", "debian", "nixos", "opensuse",
"windows", "apple", "linux", "steam", "ubuntu", "fedora", "arch", "debian", "nixos",
"opensuse", "bazzite", "cachyos", "nobara",
];
/// The card's OS glyph for an advertised chain, or `None` (no widget) when the host doesn't
Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 174 B

+7 -2
View File
@@ -8,8 +8,10 @@
use std::path::PathBuf;
use std::sync::OnceLock;
/// Embedded PNG per icon token, most-generic set the chain walk can land on. A distro
/// without its own mark (Bazzite, CachyOS, ...) degrades to its family's and finally Tux.
/// Embedded PNG per icon token: the families the chain walk can land on, plus the gaming
/// distros that earn their own mark because "a Bazzite box" and "a Fedora box" are
/// different machines to the person reading the tile. A distro with no mark of its own
/// still degrades to its family's and finally to Tux.
const ICONS: &[(&str, &[u8])] = &[
("windows", include_bytes!("../../assets/os/windows.png")),
("apple", include_bytes!("../../assets/os/apple.png")),
@@ -21,6 +23,9 @@ const ICONS: &[(&str, &[u8])] = &[
("debian", include_bytes!("../../assets/os/debian.png")),
("nixos", include_bytes!("../../assets/os/nixos.png")),
("opensuse", include_bytes!("../../assets/os/opensuse.png")),
("bazzite", include_bytes!("../../assets/os/bazzite.png")),
("cachyos", include_bytes!("../../assets/os/cachyos.png")),
("nobara", include_bytes!("../../assets/os/nobara.png")),
];
fn dir() -> Option<PathBuf> {
+13
View File
@@ -183,6 +183,19 @@ mod tests {
assert_eq!(chain, "linux/arch/cachyos");
}
#[test]
fn nobara_takes_rhel_the_first_recognized_ancestor() {
// Verified against a real Nobara 44 box: it declares `ID_LIKE="rhel centos fedora"`,
// so the family token is *rhel*, not the fedora one might expect. Clients ship a
// `nobara` mark, so the leaf matches before the family ever gets a say — but a
// RHEL-family distro we have no art for lands on plain Tux, not on Fedora.
let (chain, pretty) = parsed(
"NAME=\"Nobara Linux\"\nID=nobara\nID_LIKE=\"rhel centos fedora\"\nPRETTY_NAME=\"Nobara Linux 44 (KDE Plasma Desktop Edition)\"\n",
);
assert_eq!(chain, "linux/rhel/nobara");
assert_eq!(pretty, "Nobara Linux 44 (KDE Plasma Desktop Edition)");
}
#[test]
fn nixos_without_id_like_is_two_segments() {
let (chain, _) = parsed("ID=nixos\nPRETTY_NAME=\"NixOS 25.05 (Warbler)\"\n");
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Derive the per-client host-OS-icon assets from the assets/os-icons masters.
#
# The masters are monochrome `fill="currentColor"` SVGs, one per icon token of the host's
# OS-identity chain (see assets/os-icons/README.md). Three clients need a baked derivative
# because they cannot consume the master directly:
#
# GTK shell symbolic SVG, black fill -> clients/linux/data/icons/scalable/actions/
# Windows shell PNG, h=32, mid-grey -> clients/windows/assets/os/
# Apple clients vector PDF, black fill -> clients/apple/.../OsIcons.xcassets/
#
# The web console, the Decky plugin and the Android client transcribe the master's path
# data inline instead — those are hand-kept, and this script prints them at the end so a
# new token can be pasted straight in.
#
# Idempotent. Usage: bash scripts/gen-os-icons.sh [token ...] (default: every master)
set -euo pipefail
cd "$(dirname "$0")/.."
MASTERS=assets/os-icons
GTK=clients/linux/data/icons/scalable/actions
WIN=clients/windows/assets/os
APPLE=clients/apple/Sources/PunktfunkKit/Resources/OsIcons.xcassets
# The Windows shell has no vector element and no theme-aware tint, so its PNGs are baked in
# one mid-grey that stays legible on both the light and the dark WinUI theme.
WIN_GREY='#8A8F98'
WIN_HEIGHT=32
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
command -v rsvg-convert >/dev/null 2>&1 || {
echo "rsvg-convert not found (brew install librsvg / apt install librsvg2-bin)" >&2
exit 1
}
tokens=("$@")
if [ ${#tokens[@]} -eq 0 ]; then
for f in "$MASTERS"/*.svg; do tokens+=("$(basename "$f" .svg)"); done
fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
for t in "${tokens[@]}"; do
src="$MASTERS/$t.svg"
[ -f "$src" ] || { echo "no master for token '$t' ($src)" >&2; exit 1; }
log "$t"
# GTK: the master with the fill resolved to black — Adwaita recolors a `-symbolic` icon
# from the fill it finds, so the value only has to be a real colour, not the final one.
sed 's/currentColor/#000000/' "$src" > "$GTK/pf-os-$t-symbolic.svg"
# Windows: same black-to-grey substitution, rasterized at a fixed height so every mark
# shares an optical size and keeps its own aspect ratio.
sed "s/currentColor/$WIN_GREY/" "$src" > "$tmp/$t.grey.svg"
rsvg-convert -h "$WIN_HEIGHT" -f png -o "$WIN/$t.png" "$tmp/$t.grey.svg"
# Apple: a vector PDF at the master's natural size, in a template imageset — SwiftUI
# tints it from foregroundStyle, so the baked colour is irrelevant.
sed 's/currentColor/#000000/' "$src" > "$tmp/$t.black.svg"
mkdir -p "$APPLE/os-$t.imageset"
rsvg-convert -f pdf -o "$APPLE/os-$t.imageset/$t.pdf" "$tmp/$t.black.svg"
cat > "$APPLE/os-$t.imageset/Contents.json" <<JSON
{
"images" : [
{ "filename" : "$t.pdf", "idiom" : "universal" }
],
"info" : { "author" : "xcode", "version" : 1 },
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
JSON
done
echo
log "Inline path data (web/src/components/os-icon.tsx, clients/decky/src/os-icon.tsx,"
log " clients/android/.../components/OsIcons.kt — hand-kept, paste from here)"
for t in "${tokens[@]}"; do
python3 - "$MASTERS/$t.svg" "$t" <<'PY'
import re, sys
svg = open(sys.argv[1]).read()
box = re.search(r'viewBox="([^"]+)"', svg).group(1)
d = re.search(r'<path[^>]* d="([^"]+)"', svg).group(1)
w, h = box.split()[2:]
print(f'\n {sys.argv[2]}: viewBox "{box}" (viewport {w} x {h})\n {d}')
PY
done
echo
log "Remember: a NEW token also has to be added to each client's shipped-token list —"
log " clients/linux/src/ui_hosts.rs, clients/linux/data/resources.gresource.xml,"
log " clients/windows/src/app/os_icons.rs, clients/apple/.../PunktfunkKit/OsIcon.swift,"
log " plus the three inline registries above."
+5 -2
View File
@@ -68,14 +68,17 @@ VENDORED_TREES = [
("Vulkan-Headers (vendored, crates/pyrowave-sys)",
"crates/pyrowave-sys/vendor/pyrowave/Granite/third_party/khronos/vulkan-headers/LICENSE.md",
"https://github.com/KhronosGroup/Vulkan-Headers"),
# OS brand marks for the host-card OS icon (assets/os-icons/, CC BY 4.0 / CC0
# see assets/os-icons/README.md; clients embed per-platform derivatives).
# OS brand marks for the host-card OS icon (assets/os-icons/, CC BY 4.0 / CC0 /
# Apache-2.0 — see assets/os-icons/README.md; clients embed per-platform derivatives).
("Font Awesome Free brand icons (vendored, assets/os-icons)",
"assets/os-icons/LICENSES/font-awesome-brands.txt",
"https://fontawesome.com"),
("Simple Icons (vendored, assets/os-icons)",
"assets/os-icons/LICENSES/simple-icons.txt",
"https://simpleicons.org"),
("Bazzite logo (vendored, assets/os-icons)",
"assets/os-icons/LICENSES/bazzite.txt",
"https://github.com/ublue-os/bazzite"),
]
+22 -6
View File
@@ -1,15 +1,16 @@
// Host OS icon: the web-console derivative of the assets/os-icons masters (Font Awesome
// Free brands CC BY 4.0 + Simple Icons CC0 — provenance in assets/os-icons/README.md;
// lucide deliberately ships no brand marks). Follows the curated-registry pattern of
// api/plugins.ts: a fixed token set, resolved by walking the host's OS-identity chain.
// Host OS icon: the web-console derivative of the assets/os-icons masters (per-mark
// provenance and licensing in assets/os-icons/README.md; lucide deliberately ships no
// brand marks). Follows the curated-registry pattern of api/plugins.ts: a fixed token set,
// resolved by walking the host's OS-identity chain. Path data is transcribed from the
// masters — `bash scripts/gen-os-icons.sh <token>` prints it ready to paste.
import { Monitor } from "lucide-react";
import type { FC } from "react";
/** One monochrome brand mark: original per-icon viewBox, drawn in currentColor. */
const OS_ICONS: Record<string, { viewBox: string; d: string }> = {
windows: {
viewBox: "0 0 448 512",
d: "M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z",
viewBox: "0 0 24 24",
d: "M0 0h11.377v11.377H0zm12.623 0H24v11.377H12.623zM0 12.623h11.377V24H0zm12.623 0H24V24H12.623z",
},
apple: {
viewBox: "0 0 384 512",
@@ -47,6 +48,21 @@ const OS_ICONS: Record<string, { viewBox: string; d: string }> = {
viewBox: "0 0 640 512",
d: "M471.08 102.66s-.3 18.3-.3 20.3c-9.1-3-74.4-24.1-135.7-26.3-51.9-1.8-122.8-4.3-223 57.3-19.4 12.4-73.9 46.1-99.6 109.7C7 277-.12 307 7 335.06a111 111 0 0 0 16.5 35.7c17.4 25 46.6 41.6 78.1 44.4 44.4 3.9 78.1-16 90-53.3 8.2-25.8 0-63.6-31.5-82.9-25.6-15.7-53.3-12.1-69.2-1.6-13.9 9.2-21.8 23.5-21.6 39.2.3 27.8 24.3 42.6 41.5 42.6a49 49 0 0 0 15.8-2.7c6.5-1.8 13.3-6.5 13.3-14.9 0-12.1-11.6-14.8-16.8-13.9-2.9.5-4.5 2-11.8 2.4-2-.2-12-3.1-12-14V316c.2-12.3 13.2-18 25.5-16.9 32.3 2.8 47.7 40.7 28.5 65.7-18.3 23.7-76.6 23.2-99.7-20.4-26-49.2 12.7-111.2 87-98.4 33.2 5.7 83.6 35.5 102.4 104.3h45.9c-5.7-17.6-8.9-68.3 42.7-68.3 56.7 0 63.9 39.9 79.8 68.3H460c-12.8-18.3-21.7-38.7-18.9-55.8 5.6-33.8 39.7-18.4 82.4-17.4 66.5.4 102.1-27 103.1-28 3.7-3.1 6.5-15.8 7-17.7 1.3-5.1-3.2-2.4-3.2-2.4-8.7 5.2-30.5 15.2-50.9 15.6-25.3.5-76.2-25.4-81.6-28.2-.3-.4.1 1.2-11-25.5 88.4 58.3 118.3 40.5 145.2 21.7.8-.6 4.3-2.9 3.6-5.7-13.8-48.1-22.4-62.7-34.5-69.6-37-21.6-125-34.7-129.2-35.3.1-.1-.9-.3-.9.7zm60.4 72.8a37.54 37.54 0 0 1 38.9-36.3c33.4 1.2 48.8 42.3 24.4 65.2-24.2 22.7-64.4 4.6-63.3-28.9zm38.6-25.3a26.27 26.27 0 1 0 25.4 27.2 26.19 26.19 0 0 0-25.4-27.2zm4.3 28.8c-15.4 0-15.4-15.6 0-15.6s15.4 15.64 0 15.64z",
},
// The gaming distros get their own mark rather than their family's: "a Bazzite box" and
// "a Fedora box" are different machines to the person reading the card, and these three
// are what this project's hosts actually run.
bazzite: {
viewBox: "0 0 24 24",
d: "M7.178 0h3.589v7.178h7.524c3.153 0 5.709 2.556 5.709 5.709 0 6.138-4.976 11.113-11.113 11.113-3.153 0-5.709-2.556-5.709-5.709V10.766H0v-3.589h7.178zm3.589 10.766v7.524c0 1.171.949 2.12 2.12 2.12 4.156 0 7.524-3.369 7.524-7.524 0-1.171-.949-2.12-2.12-2.12z",
},
cachyos: {
viewBox: "0 0 24 24",
d: "M5.301 2.646 0 11.771l5.541 9.583h11.486l2.904-5.017H8.102l-2.56-4.429L8.067 7.54h6.063l2.83-4.893ZM20.058 4.12a.748.748 0 0 0 0 1.496.748.748 0 0 0 0-1.496m-1.983 4.303a1.45 1.45 0 0 0 0 2.9 1.45 1.45 0 0 0 0-2.9m4.02 3.98a1.904 1.904 0 0 0 0 3.809 1.904 1.904 0 0 0 0-3.81",
},
nobara: {
viewBox: "0 0 24 24",
d: "M23.808 11.808v8.281a3.542 3.542 0 0 1-3.542 3.527h-.46a3.543 3.543 0 0 1-3.083-3.513v-7.282l3.543-1.013-3.66-1.045a4.724 4.724 0 0 0-9.33 1.045v2.362a2.362 2.362 0 0 0 2.362 2.362 3.543 3.543 0 0 1 3.543 3.542V24a3.539 3.539 0 0 0-3.542-3.542 3.537 3.537 0 0 0-3.063 1.76 3.54 3.54 0 0 1-2.382 1.398h-.46A3.542 3.542 0 0 1 .192 20.09V3.543a3.542 3.542 0 0 1 6.323-2.194A11.756 11.756 0 0 1 12 0c6.521 0 11.808 5.287 11.808 11.808zm-9.446 0A2.359 2.359 0 0 1 12 14.17a2.362 2.362 0 1 1 2.362-2.362z",
},
};
/** `macos` renders the apple mark, `steamos` the steam mark — same aliases as every client. */
+12
View File
@@ -33,6 +33,18 @@ export const WindowsHost: Story = {
},
};
/** A gaming distro wears its OWN mark, not its family's: `cachyos` is resolved before the
* `arch` it descends from, which is the whole point of shipping art for the leaf. */
export const CachyOsHost: Story = {
args: {
host: {
data: { ...hostInfo, os: "linux/arch/cachyos", os_name: "CachyOS Linux" },
isLoading: false,
error: null,
},
},
};
/** An unrecognized distro chain walks up to its family mark here neither `chimera` nor
* `frontier` have art, so the icon degrades all the way to generic Tux. */
export const UnknownDistro: Story = {