diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 640a6fc..8099700 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -36,9 +36,9 @@ body: attributes: label: Scope estimate (your best guess) options: - - Small — isolated UI tweak, setting, or doc change - - Medium — new panel, new config surface, extra API call - - Large — architectural change, new auth mode, new platform + - Small: isolated UI tweak, setting, or doc change + - Medium: new panel, new config surface, extra API call + - Large: architectural change, new auth mode, new platform validations: required: true diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index 261e063..d5e6bd1 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -1,13 +1,13 @@ # Bump the colony-bin AUR package to a given release tag. Uses a dedicated SSH # key (AUR_SSH_PRIVATE_KEY secret) registered as a secondary key on the -# MotherSphere AUR account — scope is limited to AUR push access, revocable by +# MotherSphere AUR account - scope is limited to AUR push access, revocable by # removing that key from the AUR profile if ever compromised. # # Invoked as a reusable workflow from release.yml after the release # assets are built, uploaded and signed. It deliberately does NOT trigger on # `release: published`: our releases are created by release-please with the # default GITHUB_TOKEN, and GitHub suppresses workflow triggers for events -# created by that token — the old trigger never fired once (colony-bin sat at +# created by that token - the old trigger never fired once (colony-bin sat at # 0.1.4 for nine releases). workflow_dispatch remains as a manual fallback. name: Publish colony-bin to AUR @@ -55,7 +55,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} TAG: ${{ steps.tag.outputs.tag }} - # gh fails hard when the asset is missing — unlike the old bare curl, + # gh fails hard when the asset is missing - unlike the old bare curl, # which would happily save GitHub's 404 page and push its sha256 to # AUR, breaking installs for every colony-bin user. run: | @@ -110,7 +110,7 @@ jobs: echo "AUR currently at pkgver=${CURRENT}, target=${PKGVER}" if [ "${CURRENT}" = "${PKGVER}" ]; then echo "changed=false" >> "$GITHUB_OUTPUT" - echo "pkgver already at ${PKGVER} — nothing to do." + echo "pkgver already at ${PKGVER}, nothing to do." else echo "changed=true" >> "$GITHUB_OUTPUT" # Carried to the next step so it can rewrite every occurrence of @@ -126,7 +126,7 @@ jobs: OLDVER: ${{ steps.check.outputs.oldver }} run: | cd aur-repo - # Reset pkgrel to 1 because pkgver changed — standard AUR convention. + # Reset pkgrel to 1 because pkgver changed - standard AUR convention. sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" PKGBUILD sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD # The binary hash lives in its own _binsha variable (the icon's @@ -170,7 +170,7 @@ jobs: cd aur-repo git add PKGBUILD .SRCINFO if git diff --cached --quiet; then - echo "No changes — colony-bin already at ${PKGVER}. Skipping push." + echo "No changes: colony-bin already at ${PKGVER}. Skipping push." exit 0 fi git commit -m "colony-bin: auto-bump to ${PKGVER}" diff --git a/.gitignore b/.gitignore index bee693b..53eaa21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,2 @@ /target **/*.rs.bk - -# Local audit/workflow scratch state (never committed) -/.audit-tmp/ - -# Playwright MCP scratch -/.playwright-mcp/ -.claude/ diff --git a/Cargo.lock b/Cargo.lock index a57a9b7..48ad59e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,7 +237,7 @@ checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -272,7 +272,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -289,9 +289,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" dependencies = [ "aws-lc-sys", "zeroize", @@ -299,9 +299,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" dependencies = [ "cc", "cmake", @@ -430,7 +430,7 @@ checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -512,9 +512,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "jobserver", @@ -635,7 +635,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "shell-words", "tar", "tempfile", @@ -1013,7 +1013,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1075,7 +1075,7 @@ checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", - "sha2 0.11.0", + "sha2 0.11.1", "subtle", ] @@ -1122,9 +1122,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "errno" @@ -1315,7 +1315,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1414,7 +1414,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2274,9 +2274,9 @@ dependencies = [ [[package]] name = "keyring-core" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" +checksum = "29fbf4b551298c67925f38cf341a1ada797a75f1837bdf8ae1fc8e2ebc6efb59" dependencies = [ "log", ] @@ -2393,9 +2393,9 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" [[package]] name = "lock_api" @@ -2621,6 +2621,18 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3737,9 +3749,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "once_cell", @@ -3751,9 +3763,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3927,15 +3939,16 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ + "foldhash 0.2.0", "indexmap", "itoa", "memchr", @@ -3952,7 +3965,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3989,9 +4002,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures 0.3.1", @@ -4094,9 +4107,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "smithay-client-toolkit" @@ -4274,9 +4287,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -4300,7 +4313,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -4403,7 +4416,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -4471,9 +4484,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.2" +version = "1.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" dependencies = [ "bytes", "libc", @@ -4717,9 +4730,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-linebreak" @@ -4875,7 +4888,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -5605,9 +5618,9 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.31" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b" dependencies = [ "version_check", ] @@ -5722,7 +5735,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] @@ -5770,7 +5783,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "zbus_names", "zvariant", "zvariant_utils", @@ -5804,18 +5817,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", @@ -5839,7 +5852,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure", ] @@ -5893,19 +5906,20 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "zip" -version = "9.0.0" +version = "9.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711e8c9646791c49b7eb799a19c4ae70a9b8372ddc6a1fa4778defd1e2aa5d5e" +checksum = "c2d98561b612a84ae525331a7a094429ef8a5772514ed507ff6db5f54badc971" dependencies = [ "crc32fast", "flate2", "indexmap", "memchr", + "nix", "typed-path", "zopfli", ] @@ -5924,9 +5938,9 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" dependencies = [ "bumpalo", "crc32fast", @@ -5958,7 +5972,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "zvariant_utils", ] @@ -5971,6 +5985,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 3.0.6", + "syn 3.0.9", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 6175e12..1377e42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,10 @@ colony-ui = "0.1.6" iced = { version = "0.14.0", features = ["tokio", "markdown", "advanced", "image-without-codecs"] } shell-words = "1.1.1" serde = { version = "1.0.229", features = ["derive"] } -serde_json = "1.0.151" +serde_json = "1.0.154" toml = "1.1.8" reqwest = { version = "0.13.5", default-features = false, features = ["json", "form", "stream", "rustls", "http2", "charset", "system-proxy"] } -tokio = { version = "1.53.2", features = ["sync", "time", "rt"] } +tokio = { version = "1.53.3", features = ["sync", "time", "rt"] } futures = "0.3.34" open = "5.4.4" base64 = "0.23.1" @@ -40,12 +40,12 @@ dirs = "7.0.0" # no implicit store at all. Colony never sets a default store and builds every # entry from the per-target store in src/oauth.rs, so the mock is reachable only # from the tests, and the plaintext copy goes only after the token reads back. -keyring-core = "1.0.0" +keyring-core = "1.0.1" semver = "1.0.28" -sha2 = "0.11.0" +sha2 = "0.11.1" # Pure-Rust ed25519 verification for signed launcher self-updates (no openssl). ed25519-dalek = { version = "3.0.0", default-features = false } -zip = { version = "9.0.0", default-features = false, features = ["deflate"] } +zip = { version = "9.0.3", default-features = false, features = ["deflate"] } tar = "0.4.46" flate2 = "1.1.10" # PNG-only decoder for per-app icons (no other codecs); pairs with iced's @@ -88,4 +88,4 @@ tempfile = "3.27.0" # Embeds the Windows version resource (build.rs). Without default features it # pulls in nothing but version_check, and it does nothing on other targets. [build-dependencies] -winresource = { version = "0.1.31", default-features = false } +winresource = { version = "0.1.34", default-features = false } diff --git a/README.md b/README.md index 947b44a..2698187 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,10 @@ to the applications already on your system. > and never an older version than the one running. CI builds and tests every > change on Linux, Windows and macOS, and checks that it still builds with Rust > 1.88. The Windows and macOS builds work but are best-effort (see -> [Platforms](#platforms)). Releases now go through the organisation's shared -> sign-and-publish workflow, which has not yet published a Colony release: the -> latest, v0.10.4, was signed by the workflow it replaced. Authenticode signing -> of the Windows build through SignPath is applied for and not active yet. Every +> [Platforms](#platforms)). v0.11.0 is the first release built, signed and +> published by the organisation's shared sign-and-publish workflow, and the +> same run bumped the `colony-bin` AUR package. Authenticode signing of the +> Windows build through SignPath is applied for and not active yet. Every > Project Colony program's release downloads the latest `colony-linux`, verifies > its signatures and runs its `validate-manifest`, so a broken Colony release > blocks the releases of every other program. @@ -80,6 +80,26 @@ Project-Colony on GitHub Colony your machine motion, English or French, keyboard navigation, and the storage and update settings. +### Themes + +Colony ships with **26 theme families and 59 palettes**, compiled into the +binary. They come from +[colony-ui](https://crates.io/crates/colony-ui) and are shared with every other +Project Colony program, so a theme looks the same wherever you meet it: + +| | | | | +|---|---|---|---| +| Catppuccin (Latte, Frappé, Macchiato, Mocha) | Gruvbox | Everblush | Kanagawa (Lotus, Wave, Journal, Dragon) | +| Nord | Dracula | Solarized | Tokyo Night | +| Rosé Pine (Main, Moon, Dawn) | One Dark | Monokai Pro (Pro, Classic, Spectrum) | Ayu (Dark, Mirage, Light) | +| Everforest | Material (Oceanic, Palenight, Deep Ocean) | Flexoki | Nightfox | +| Sonokai | Oxocarbon | Night Owl | Iceberg | +| Horizon | Mélange | Synthwave '84 | Modus (Operandi, Vivendi) | +| Stellar Blade (Eve, Tachy, Lily, Enya, Kaya; fan-made, unofficial) | Parchment | | | + +Each palette includes full semantic tokens: backgrounds, text layers, accents, +success, warning and error states, button states, and more. + New to Colony? The [tutorial](docs/guide/tutorial.md) walks through a first install. @@ -132,67 +152,7 @@ Requires Rust 1.88 or newer and a C compiler. No system libraries are needed: on Linux, the D-Bus client Colony uses for the Secret Service is compiled from source. -## Design principles - -**Single purpose, native performance.** Colony and every app in its ecosystem -follow the same philosophy: - -- **Rust-native**: built with [Iced](https://iced.rs). Startup is instant, - memory use is small, and your CPU stays cool. -- **Async everything**: network calls, file I/O and scanning run in the - background. The UI never freezes. -- **Zero configuration needed**: Colony works out of the box. Scan directories, - sidebar sections and themes can all be changed, but the defaults are sensible. - -## Theming - -Colony ships with **26 theme families and 59 palettes**, compiled into the -binary with zero runtime cost. They come from -[colony-ui](https://crates.io/crates/colony-ui) and are shared with every other -Project Colony program, so a theme looks the same wherever you meet it: - -| | | | | -|---|---|---|---| -| Catppuccin (Latte, Frappé, Macchiato, Mocha) | Gruvbox | Everblush | Kanagawa (Lotus, Wave, Journal, Dragon) | -| Nord | Dracula | Solarized | Tokyo Night | -| Rosé Pine (Main, Moon, Dawn) | One Dark | Monokai Pro (Pro, Classic, Spectrum) | Ayu (Dark, Mirage, Light) | -| Everforest | Material (Oceanic, Palenight, Deep Ocean) | Flexoki | Nightfox | -| Sonokai | Oxocarbon | Night Owl | Iceberg | -| Horizon | Mélange | Synthwave '84 | Modus (Operandi, Vivendi) | -| Stellar Blade (Eve, Tachy, Lily, Enya, Kaya; fan-made, unofficial) | Parchment | | | - -Each palette includes full semantic tokens: backgrounds, text layers, accents, -success, warning and error states, button states, and more. - -## For app developers - -Want your desktop app to appear in Colony's catalog? It takes one JSON file plus -a GitHub release with properly named assets. Colony only lists repositories of -the [Project-Colony](https://github.com/Project-Colony) GitHub organisation, so -the repository has to live there. - -**Quick version**: add `colony.json` at the root of your repository: - -```json -{ - "$schema": "https://raw.githubusercontent.com/Project-Colony/Project-Colony-Resources/main/generated/colony.schema.json", - "name": "YourApp", - "category": "utility" -} -``` - -Then publish a GitHub release with assets named `yourapp-linux`, -`yourapp-windows.exe`, `yourapp-macos` and `yourapp-macos-x86`. Colony picks -them up automatically. - -**Full walkthrough**: [CONTRIBUTING.md, Adding your app to Colony](CONTRIBUTING.md#adding-your-app-to-colony), -and the [colony.json reference](docs/guide/colony-spec.md). - -The canonical [release workflow template](https://github.com/Project-Colony/Project-Colony-Resources/blob/main/templates/sign-and-publish-caller.yml) -lives in Project-Colony-Resources, alongside the shared signing workflow it calls -and the manifest schema. - -## Platforms +### Platforms Colony is developed and tested on Linux. Windows and macOS builds are produced by the same CI, install and update apps, and are genuinely usable, but they get @@ -215,10 +175,15 @@ that is how these move up. Full documentation is in [docs/](docs/). Start at [docs/README.md](docs/README.md). -## Code signing policy +Want your app in Colony's catalog? It takes a `colony.json` at the root of a +repository of the [Project-Colony](https://github.com/Project-Colony) +organisation and a release with properly named, signed files. See +[CONTRIBUTING.md, Adding your app to Colony](CONTRIBUTING.md#adding-your-app-to-colony), +the [colony.json reference](docs/guide/colony-spec.md) and the +[release workflow template](https://github.com/Project-Colony/Project-Colony-Resources/blob/main/templates/sign-and-publish-caller.yml) +in Project-Colony-Resources. -Free code signing provided by [SignPath.io](https://signpath.io), certificate by -[SignPath Foundation](https://signpath.org). +## Code signing policy Windows builds will be Authenticode-signed once SignPath Foundation has accepted the project; until then they ship without Authenticode. Every release asset, on @@ -255,12 +220,20 @@ How releases are built, signed and published: [design/releases.md](https://github.com/Project-Colony/Project-Colony-Resources/blob/main/design/releases.md#5-signing), and for Colony's side of it, [docs/internals/release-signing.md](docs/internals/release-signing.md). -Team roles and members: + -### Privacy policy +## Privacy Colony talks to GitHub and to no other server. It has no telemetry, no analytics and no crash reporting. diff --git a/SECURITY.md b/SECURITY.md index 08f081f..94793dc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -24,8 +24,9 @@ What to expect: - **Acknowledgement** within a few days. - A fix (or a mitigation plan) before any public disclosure, coordinated with - you. Given the project's release automation, a patched release usually ships - as soon as the fix lands. + you. A fix ships when its release pull request is merged: the release + workflow then builds, signs and publishes the patched version, and Colony + offers it as a self-update. - Credit in the release notes if you want it. ## Scope and trust model diff --git a/src/app.rs b/src/app.rs index 0f24197..69f9bdd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -534,12 +534,21 @@ impl App { } } + /// The iced theme built from the active Colony palette, so the widgets that + /// keep iced's default styling (scrollbars, Markdown code blocks, text + /// selection) follow the chosen theme, accent and high contrast instead of + /// iced's stock Light or Dark. pub(crate) fn theme(&self) -> Theme { - use colony_ui::ColorExt; - if ui::theme::active_palette().bg_primary.is_light() { - Theme::Light - } else { - Theme::Dark - } + Theme::custom( + "Colony", + iced::theme::Palette { + background: Palette::BG_PRIMARY(), + text: Palette::TEXT_PRIMARY(), + primary: Palette::ACCENT(), + success: Palette::SUCCESS(), + warning: Palette::WARNING(), + danger: Palette::ERROR(), + }, + ) } } diff --git a/src/download/files.rs b/src/download/files.rs index b90993a..719b619 100644 --- a/src/download/files.rs +++ b/src/download/files.rs @@ -202,7 +202,7 @@ fn extract_from_tar_gz( /// Extract a binary from an archive based on its extension. /// /// `asset_name` is the original release asset filename used only to detect the -/// archive type — `archive_path` may be a staging file (e.g. `foo.zip.part`) +/// archive type - `archive_path` may be a staging file (e.g. `foo.zip.part`) /// whose own extension must not be used for detection. pub(super) fn extract_binary_from_archive( archive_path: &std::path::Path, @@ -213,9 +213,9 @@ pub(super) fn extract_binary_from_archive( // `binary_name` is a manifest-supplied string that becomes a path component // in EVERY branch below, so the traversal guard is hoisted here, before the // dispatch. It used to live only inside the zip and tar extractors, which - // left the raw-binary branch able to write outside `dest_dir` — and, after - // the caller's chmod 0755 and desktop entry, to gain execution at next login - // — from a hostile manifest. The extractors keep their own call so they stay + // left the raw-binary branch able to write outside `dest_dir` (and, after + // the caller's chmod 0755 and desktop entry, to gain execution at next login) + // from a hostile manifest. The extractors keep their own call so they stay // safe in isolation. ensure_safe_component(binary_name)?; if asset_name.ends_with(".zip") { @@ -227,7 +227,7 @@ pub(super) fn extract_binary_from_archive( let _ = std::fs::remove_file(archive_path); result } else { - // Raw binary (e.g. .exe, no archive extension) — rename to binary_name in dest_dir + // Raw binary (e.g. .exe, no archive extension) - rename to binary_name in dest_dir let dest = dest_dir.join(binary_name); replace_file(archive_path, &dest)?; Ok(dest) diff --git a/src/download/http.rs b/src/download/http.rs index c42cca6..3d0fe7f 100644 --- a/src/download/http.rs +++ b/src/download/http.rs @@ -441,7 +441,7 @@ pub(crate) fn build_url(base: &str, segments: &[&str]) -> Result { /// /// Gate for everything handed to the desktop's URI opener: link destinations in /// remote Markdown (READMEs, release notes) reach it verbatim, and `open::that` -/// is not a browser call — it execs `xdg-open`/`gio open` on Linux and +/// is not a browser call - it execs `xdg-open`/`gio open` on Linux and /// `Start-Process` on Windows, both of which dispatch `file://`, UNC paths and /// any registered `x-scheme-handler/*`. A `file://` or custom-scheme link in a /// hostile README would otherwise be a one-click execution primitive that diff --git a/src/download/install.rs b/src/download/install.rs index ae4ae59..77b0aff 100644 --- a/src/download/install.rs +++ b/src/download/install.rs @@ -58,7 +58,7 @@ pub async fn download_release_asset( record_asset, require_signature, } = install; - // The manifest-supplied filename becomes a local path — guard it against + // The manifest-supplied filename becomes a local path - guard it against // traversal (`../`, absolute paths) before joining, mirroring the archive // `binary` guard. ensure_safe_component(&filename)?; @@ -194,7 +194,7 @@ pub async fn download_release_asset( } // Integrity check, archive extraction and the atomic promotion are - // CPU/IO-bound — run them on a blocking thread. Any failure removes the + // CPU/IO-bound - run them on a blocking thread. Any failure removes the // temp file and leaves the previous install untouched. let final_path = { let temp_path = temp_path.clone(); diff --git a/src/download/launcher.rs b/src/download/launcher.rs index 5176f70..211ba1a 100644 --- a/src/download/launcher.rs +++ b/src/download/launcher.rs @@ -132,7 +132,7 @@ pub async fn download_launcher_asset( } // Persist the signature next to the staged binary so apply_launcher_update - // can re-verify at install time — closing any window in which the staged + // can re-verify at install time - closing any window in which the staged // file could be swapped between download and apply. The metadata sidecar is // staged for the same reason. let sig_path = staged_signature_path(&dest_path); diff --git a/src/github/catalog.rs b/src/github/catalog.rs index 5a4e5e0..64b88a3 100644 --- a/src/github/catalog.rs +++ b/src/github/catalog.rs @@ -264,7 +264,7 @@ async fn fetch_readme(client: &reqwest::Client, repo_name: &str) -> Result Result> { @@ -288,7 +288,7 @@ async fn fetch_license(client: &reqwest::Client, repo_name: &str) -> Result Result { - // Not Modified — return cached body + // Not Modified - return cached body if let Ok(cache) = HTTP_CACHE.lock() { if let Some(entry) = cache.entries.get(url) { tracing::debug!("Cache hit (304) for {}", url); diff --git a/src/github/releases.rs b/src/github/releases.rs index d3cab92..001bd73 100644 --- a/src/github/releases.rs +++ b/src/github/releases.rs @@ -304,7 +304,7 @@ pub fn parse_version_tag(tag: &str) -> Option { /// `Ok(Some(tag))` = an update to `tag` is available. `Ok(None)` = the check /// RAN and there is nothing to install (either the app is not installed at all, /// or it is current). `Err` = the check could NOT run, and the caller must not -/// turn that into "up to date" — the same fail-loud contract +/// turn that into "up to date" - the same fail-loud contract /// [`check_launcher_update`] already keeps for the launcher's own check. /// /// `pinned_tag` is compared directly unless it is "latest", in which case the diff --git a/src/github/types.rs b/src/github/types.rs index 9373124..6cb1975 100644 --- a/src/github/types.rs +++ b/src/github/types.rs @@ -13,7 +13,7 @@ pub struct ReleaseFileEntry { pub file: Option, /// Substring pattern to match against release asset names (case-insensitive). /// Colony fetches the release assets list and picks the one matching this pattern. - /// Mutually exclusive with `file` — use one or the other. + /// Mutually exclusive with `file` - use one or the other. pub file_pattern: Option, /// Optional binary name inside an archive. When present, the downloaded file /// is treated as an archive (.zip / .tar.gz) and Colony extracts this binary. diff --git a/src/i18n/en.rs b/src/i18n/en.rs index e72cdac..22165cc 100644 --- a/src/i18n/en.rs +++ b/src/i18n/en.rs @@ -29,7 +29,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { ); strings.insert( "github_copy_hint".into(), - "Click to copy — Waiting for authorization...".into(), + "Click to copy. Waiting for authorization...".into(), ); strings.insert("github_connecting".into(), "Connecting...".into()); strings.insert("github_connected".into(), "Connected".into()); @@ -97,7 +97,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { ); strings.insert( "oauth_device_failed".into(), - "GitHub sign-in failed: {error} — {desc}".into(), + "GitHub sign-in failed: {error} ({desc})".into(), ); strings.insert("github_api_error".into(), "GitHub error: {error}".into()); strings.insert("scan_error".into(), "Error: {error}".into()); @@ -120,7 +120,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { ); strings.insert( "update_check_failed".into(), - "Could not check {count} app(s) for updates — they may not be up to date".into(), + "Could not check {count} app(s) for updates, so they may not be up to date".into(), ); // Sidebar section names (localized) @@ -159,11 +159,11 @@ pub(super) fn insert_all(strings: &mut HashMap) { // Favorites strings.insert("add_favorite".into(), "Add to favorites".into()); - // First launch — carousel (3 steps) + // First launch: carousel (3 steps) strings.insert("welcome_title".into(), "Welcome to Colony".into()); strings.insert("welcome_desc".into(), "The centralized launcher for the Project-Colony ecosystem. Discover, install and launch apps in one click.".into()); - // Step 1 — interface tour - // Step 2 — GitHub + ready + // Step 1: interface tour + // Step 2: GitHub + ready // Navigation strings.insert("welcome_start".into(), "Let's go!".into()); strings.insert("welcome_next".into(), "Next".into()); @@ -174,7 +174,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { // Guided tutorial (spotlight over real UI) strings.insert("tut_sidebar_title".into(), "Categories".into()); - strings.insert("tut_sidebar_desc".into(), "Filter your apps by type — games, tools, favorites — or by origin (Colony ecosystem vs. system). The sidebar stays visible at all times.".into()); + strings.insert("tut_sidebar_desc".into(), "Filter your apps by type (games, tools, favorites) or by origin (the Colony ecosystem or your system). The sidebar stays visible at all times.".into()); strings.insert("tut_search_title".into(), "Search".into()); strings.insert( "tut_search_desc".into(), @@ -367,7 +367,8 @@ pub(super) fn insert_all(strings: &mut HashMap) { strings.insert("section_security".into(), "Security".into()); strings.insert("language_changed".into(), "Language changed".into()); strings.insert("clear_caches".into(), "Clear store caches".into()); - strings.insert("clear_caches_desc".into(), "Removes cached descriptions and icons (they re-download on the next refresh). Installed applications are not touched.".into()); + strings.insert("settings_section_caches".into(), "Caches".into()); + strings.insert("settings_caches_desc".into(), "Removes cached descriptions and icons (they re-download on the next refresh). Installed applications are not touched.".into()); strings.insert("caches_cleared".into(), "{count} cache(s) removed".into()); strings.insert("launcher_update_system_managed".into(), "Update {version} is available - this install is managed by the package manager, update via 'pacman -Syu' (colony-bin)".into()); // Detail tabs diff --git a/src/i18n/fr.rs b/src/i18n/fr.rs index 569b23d..81a30b6 100644 --- a/src/i18n/fr.rs +++ b/src/i18n/fr.rs @@ -28,7 +28,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { ); strings.insert( "github_copy_hint".into(), - "Cliquez pour copier — En attente d'autorisation...".into(), + "Cliquez pour copier. En attente d'autorisation...".into(), ); strings.insert("github_connecting".into(), "Connexion en cours...".into()); strings.insert("github_connected".into(), "Connecté".into()); @@ -66,7 +66,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { strings.insert("offered_version".into(), "Version {version}".into()); strings.insert( "no_release_unrecognized".into(), - "Aucune version installable - cette application ne publie pas d'assets reconnus par Colony" + "Aucune version installable : cette application ne publie aucun fichier de version reconnu par Colony" .into(), ); strings.insert( @@ -109,7 +109,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { ); strings.insert( "oauth_device_failed".into(), - "Échec de la connexion GitHub : {error} — {desc}".into(), + "Échec de la connexion GitHub : {error} ({desc})".into(), ); strings.insert("github_api_error".into(), "Erreur GitHub : {error}".into()); strings.insert("scan_error".into(), "Erreur : {error}".into()); @@ -135,7 +135,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { ); strings.insert( "update_check_failed".into(), - "Impossible de vérifier {count} application(s) — elles ne sont peut-être pas à jour".into(), + "Impossible de vérifier {count} application(s) : elles ne sont peut-être pas à jour".into(), ); // Sidebar section names (localized) @@ -174,11 +174,11 @@ pub(super) fn insert_all(strings: &mut HashMap) { // Favorites strings.insert("add_favorite".into(), "Ajouter aux favoris".into()); - // First launch — carousel (3 steps) + // First launch: carousel (3 steps) strings.insert("welcome_title".into(), "Bienvenue dans Colony".into()); strings.insert("welcome_desc".into(), "Le lanceur centralisé de l'écosystème Project-Colony. Découvrez, installez et lancez vos apps en un clic.".into()); - // Step 1 — interface tour - // Step 2 — GitHub + ready + // Step 1: interface tour + // Step 2: GitHub + ready // Navigation strings.insert("welcome_start".into(), "C'est parti !".into()); strings.insert("welcome_next".into(), "Suivant".into()); @@ -192,7 +192,7 @@ pub(super) fn insert_all(strings: &mut HashMap) { // Guided tutorial (spotlight on the real UI) strings.insert("tut_sidebar_title".into(), "Les catégories".into()); - strings.insert("tut_sidebar_desc".into(), "Filtrez vos apps par type : jeux, outils, favoris, ou par origine (écosystème Colony vs. système). La barre latérale reste toujours visible.".into()); + strings.insert("tut_sidebar_desc".into(), "Filtrez vos apps par type (jeux, outils, favoris) ou par origine (écosystème Colony ou système). La barre latérale reste toujours visible.".into()); strings.insert("tut_search_title".into(), "La recherche".into()); strings.insert("tut_search_desc".into(), "Tapez le nom d'une app pour la retrouver instantanément, peu importe la catégorie sélectionnée.".into()); strings.insert("tut_grid_title".into(), "Vos applications".into()); @@ -402,14 +402,15 @@ pub(super) fn insert_all(strings: &mut HashMap) { "clear_caches".into(), "Vider les caches du catalogue".into(), ); - strings.insert("clear_caches_desc".into(), "Supprime les descriptions et icônes mises en cache (elles sont retéléchargées à la prochaine actualisation). Les applications installées ne sont pas touchées.".into()); + strings.insert("settings_section_caches".into(), "Données en cache".into()); + strings.insert("settings_caches_desc".into(), "Supprime les descriptions et icônes mises en cache (elles sont retéléchargées à la prochaine actualisation). Les applications installées ne sont pas touchées.".into()); strings.insert( "caches_cleared".into(), "{count} cache(s) supprimé(s)".into(), ); strings.insert("launcher_update_system_managed".into(), "Mise à jour {version} disponible - cette installation est gérée par le gestionnaire de paquets, mettez à jour via « pacman -Syu » (colony-bin)".into()); // Detail tabs - strings.insert("tab_readme".into(), "ReadMe".into()); + strings.insert("tab_readme".into(), "Présentation".into()); strings.insert("tab_license".into(), "Licence".into()); strings.insert("tab_changelog".into(), "Journal des modifications".into()); strings.insert("tab_not_available".into(), "Non disponible".into()); diff --git a/src/i18n/mod.rs b/src/i18n/mod.rs index 706f4b2..55d692f 100644 --- a/src/i18n/mod.rs +++ b/src/i18n/mod.rs @@ -146,7 +146,7 @@ fn detect_language() -> String { #[cfg(test)] mod tests { /// The active locale is process-global, so tests that swap it must not run - /// concurrently with each other — the failure is a value from whichever + /// concurrently with each other - the failure is a value from whichever /// language the other test happened to set. static LOCALE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); @@ -156,7 +156,7 @@ mod tests { } /// Theme and accent labels live in colony-ui, not in en.rs / fr.rs. They - /// must still resolve through `t()`, and must follow the active locale — + /// must still resolve through `t()`, and must follow the active locale - /// otherwise the theme picker would sit in English inside a French page. #[test] fn shared_labels_resolve_through_colony_ui_and_follow_the_locale() { @@ -217,7 +217,7 @@ mod tests { let only_en: Vec<&&String> = en_keys.difference(&fr_keys).collect(); assert!( only_fr.is_empty() && only_en.is_empty(), - "Locale key mismatch — only in fr: {only_fr:?}; only in en: {only_en:?}" + "Locale key mismatch: only in fr: {only_fr:?}; only in en: {only_en:?}" ); } @@ -301,6 +301,34 @@ mod tests { ); } + /// A local app's category chip shows `section_display_name` of its + /// `AppCategory` Debug name. A variant with no section key would fall back + /// to the English enum name inside the French grid. + #[test] + fn every_app_category_chip_is_localized() { + use crate::scan::AppCategory::*; + with_locale(|| { + set_language("fr"); + for category in [ + Development, + Graphics, + Network, + Office, + Multimedia, + System, + Utility, + Security, + Game, + Other, + ] { + let raw = format!("{category:?}"); + assert_ne!(section_display_name(&raw), raw, "no French label"); + } + assert_eq!(section_display_name("utility"), "Utilitaires"); + set_language("en"); + }); + } + #[test] fn t_fmt_substitution() { with_locale(|| { diff --git a/src/message.rs b/src/message.rs index 4dffb1c..c5ad403 100644 --- a/src/message.rs +++ b/src/message.rs @@ -39,7 +39,7 @@ pub enum Message { CancelDownload, LaunchColonyApp(PathBuf), UninstallColonyApp(String), // repo_name - ConfirmUninstall(String), // repo_name — show confirmation + ConfirmUninstall(String), // repo_name - show confirmation CancelUninstall, CopyToClipboard(String), DismissNotification(u64), diff --git a/src/oauth.rs b/src/oauth.rs index 3a13be2..beef0b4 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -261,7 +261,7 @@ fn token_path() -> PathBuf { auth_dir.join("github_token.json") } // Only the creation failed; ask for the same path without creating it - // rather than rebuilding the layout by hand — spelling it out here with + // rather than rebuilding the layout by hand - spelling it out here with // dirs::config_dir() put the token in Roaming on Windows, which is the // one place the shared layout says it must not be. Err(_) => colony_ui::paths::locate::config_dir("Colony") diff --git a/src/persistence.rs b/src/persistence.rs index 3433f23..7a852a1 100644 --- a/src/persistence.rs +++ b/src/persistence.rs @@ -368,7 +368,7 @@ pub fn load_repo_icon(repo_name: &str) -> Option> { /// The shared install root: `/Colony/apps/`. /// -/// Deliberately a sibling of Colony's own directory rather than a child — +/// Deliberately a sibling of Colony's own directory rather than a child - /// installed programs belong to the ecosystem, not to the launcher. Does not /// create the directory; callers that write do that themselves. pub fn colony_apps_dir() -> Result { @@ -386,7 +386,7 @@ pub fn installed_app_path(repo: &ColonyRepo) -> Option { } else if let Some(ref file) = entry.file { file.clone() } else { - // filePattern was used — check saved resolved asset name + // filePattern was used - check saved resolved asset name load_installed_asset(&repo.name)? }; // The filename comes straight from the repo's own manifest: the same @@ -1430,14 +1430,14 @@ mod path_migration_tests { "{rel}" ); } - // The source survives a copy — that is the whole point of the fallback. + // The source survives a copy - that is the whole point of the fallback. assert!(from.join("a.json").exists()); let _ = std::fs::remove_dir_all(&root); } /// The unit tests above cover `relocate` and `copy_tree` in isolation. - /// This exercises `migrate_legacy_paths` itself — the thing that actually - /// runs on a user's machine — against a planted legacy layout. + /// This exercises `migrate_legacy_paths` itself - the thing that actually + /// runs on a user's machine - against a planted legacy layout. /// /// Linux only: it drives the XDG variables, and on Linux the config root is /// unchanged by the migration, so what it proves is the cache move, which diff --git a/src/scan.rs b/src/scan.rs index 755190d..31e7733 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -45,7 +45,7 @@ impl AppCategory { } } - /// FontAwesome (solid) glyph representing this category — one shared icon + /// FontAwesome (solid) glyph representing this category - one shared icon /// language for app tiles and, later, the sidebar. pub fn glyph(&self) -> &'static str { match self { diff --git a/src/ui/app_grid.rs b/src/ui/app_grid.rs index b691079..4305427 100644 --- a/src/ui/app_grid.rs +++ b/src/ui/app_grid.rs @@ -11,7 +11,7 @@ use crate::state::App; use crate::ui::glyph_label; use crate::ui::theme::{self, Palette}; -/// Filled Material-Design hexagon glyph from the embedded Nerd Font — the +/// Filled Material-Design hexagon glyph from the embedded Nerd Font - the /// Colony "cell" that is the app tile, the brand mark, and the status badge. const HEX_FILLED: &str = "\u{f02d8}"; @@ -101,13 +101,13 @@ impl App { /// The 54px hexagon "cell": a filled hexagon in the app's stable tint, with /// the category glyph centered on top. Both glyphs are centered via the text /// widget's own alignment (width/height Fill + centered) so they co-register - /// in the same box. Zero assets — both are embedded font glyphs. + /// in the same box. Zero assets - both are embedded font glyphs. fn hex_tile<'a>(&self, tint: Color, glyph: &'static str) -> Element<'a, Message> { use iced::alignment::{Horizontal, Vertical}; let fg = theme::contrast_on(tint); let hexagon = text(HEX_FILLED) .size(sz(50)) - .font(ui_font()) + .font(fonts::glyph_font()) .color(tint) .width(Fill) .height(Fill) @@ -115,10 +115,12 @@ impl App { .align_y(Vertical::Center); // Render the category glyph in the SAME font as the hexagon (the Nerd // Font carries the FontAwesome glyphs too), so the two share identical - // metrics and centre on the exact same point — no manual nudging. + // metrics and centre on the exact same point - no manual nudging. Both + // use the glyph font, never ui_font(): with the dyslexia font on, that + // is OpenDyslexic, which has none of these glyphs. let cat = text(glyph) .size(sz(15)) - .font(ui_font()) + .font(fonts::glyph_font()) .color(fg) .width(Fill) .height(Fill) @@ -135,7 +137,7 @@ impl App { /// The 54x54 card tile: the app's real decoded icon when we have one, /// otherwise the tinted category hexagon. Clones a cheap (Arc-backed) handle - /// out of state — never decodes in `view()`. + /// out of state - never decodes in `view()`. fn icon_tile<'a>( &self, repo_name: &str, @@ -211,7 +213,7 @@ impl App { let head = row![ text(HEX_FILLED) .size(sz(11)) - .font(ui_font()) + .font(fonts::glyph_font()) .color(Palette::WARNING()), text(crate::i18n::t("status_update")) .size(sz(12)) @@ -236,7 +238,7 @@ impl App { let head = row![ text(HEX_FILLED) .size(sz(11)) - .font(ui_font()) + .font(fonts::glyph_font()) .color(Palette::SUCCESS()), text(crate::i18n::t("status_installed")) .size(sz(12)) @@ -327,7 +329,26 @@ impl App { .on_input(Message::SearchChanged) .padding(12) .size(sz(16)) - .width(Fill); + .font(ui_font()) + .width(Fill) + .style(|_theme, status| { + let border = match status { + text_input::Status::Focused { .. } => Palette::ACCENT(), + _ => Palette::BORDER_SUBTLE(), + }; + text_input::Style { + background: Palette::BG_INPUT().into(), + border: iced::Border { + color: border, + width: 1.0, + radius: 8.0.into(), + }, + icon: Palette::TEXT_DIMMER(), + placeholder: Palette::TEXT_PLACEHOLDER(), + value: Palette::TEXT_PRIMARY(), + selection: Palette::BG_SELECTED(), + } + }); let search = container(search_input) .id(crate::ui::tutorial::ID_SEARCH) .width(Fill); @@ -574,7 +595,8 @@ impl App { }; // Chip row: category, language, present platforms. - let mut chips = row![self.chip(repo.manifest.category.clone())].spacing(7); + let mut chips = + row![self.chip(crate::i18n::section_display_name(&repo.manifest.category))].spacing(7); if !repo.language.is_empty() && repo.language != "Unknown" { chips = chips.push(self.chip(repo.language.clone())); } @@ -585,7 +607,7 @@ impl App { let mid = column![name, desc, chips].spacing(6).width(Fill); let status = container(self.repo_status(repo)) - .width(Length::Fixed(136.0)) + .width(Length::Fixed(136.0 * colony_ui::font_scale())) .align_x(iced::alignment::Horizontal::Right); let content = row![tile, mid, status] @@ -667,7 +689,8 @@ impl App { .font(ui_font_medium()) .color(Palette::TEXT_PRIMARY()); - let chips = row![self.chip(format!("{:?}", app.category))].spacing(7); + let category = crate::i18n::section_display_name(&format!("{:?}", app.category)); + let chips = row![self.chip(category)].spacing(7); let mid = column![name, chips].spacing(6).width(Fill); // Favorites were a dead feature for local apps: the section filtered @@ -727,7 +750,7 @@ impl App { .spacing(6) .align_y(iced::Alignment::Center), ) - .width(Length::Fixed(170.0)) + .width(Length::Fixed(170.0 * colony_ui::font_scale())) .align_x(iced::alignment::Horizontal::Right); let content = row![tile, mid, status] diff --git a/src/ui/detail.rs b/src/ui/detail.rs index 80289da..b7bb701 100644 --- a/src/ui/detail.rs +++ b/src/ui/detail.rs @@ -42,16 +42,54 @@ impl<'a> markdown::Viewer<'a, Message> for ColonyMdViewer { ) .width(Length::Fill) .padding(settings.code_size / 4) - .class(::code_block()) + .style(|_theme| container::Style { + background: Some(Palette::BG_CARD().into()), + text_color: Some(Palette::TEXT_PRIMARY()), + border: iced::Border { + color: Palette::BORDER_SUBTLE(), + width: 1.0, + radius: 6.0.into(), + }, + ..Default::default() + }) .into() } } +/// Markdown styling from the Colony palette and fonts. iced's +/// `Style::from_palette` sets white inline code on near-black and the system +/// monospace font whatever the theme, and its body font is the default font +/// captured at startup, so READMEs ignored the dyslexia font until a restart. +/// JetBrains Mono is already monospace, so code needs no fourth font. +fn md_style() -> markdown::Style { + let font = ui_font(); + markdown::Style { + font, + inline_code_highlight: markdown::Highlight { + background: Palette::BG_CARD().into(), + border: iced::border::rounded(4), + }, + inline_code_padding: iced::padding::left(1).right(1), + inline_code_color: Palette::TEXT_PRIMARY(), + inline_code_font: font, + code_block_font: font, + link_color: Palette::ACCENT(), + } +} + impl App { pub(crate) fn view_colony_detail<'a>(&'a self, repo: &'a ColonyRepo) -> Element<'a, Message> { let back_button = button(text(crate::i18n::t("back")).size(sz(13)).font(ui_font())) .on_press(Message::ColonyRepoBack) - .padding([8, 16]); + .padding([8, 16]) + .style(|_theme, status| { + crate::ui::theme::action_button_style( + status, + Palette::BTN_DEFAULT(), + Palette::BTN_HOVER(), + Palette::BTN_PRESSED(), + ) + }); let title = text(repo.display_name()) .size(sz(24)) @@ -102,10 +140,7 @@ impl App { // Parsed Markdown comes from self.detail_blocks, kept up-to-date by the // update-side helper. Placeholder text falls back to plain muted text. - let md_settings = markdown::Settings::with_text_size( - sz(14), - markdown::Style::from_palette(iced::Theme::Dark.palette()), - ); + let md_settings = markdown::Settings::with_text_size(sz(14), md_style()); let description: Element<'_, Message> = if is_placeholder { text(crate::i18n::t("tab_not_available")) .size(sz(14)) @@ -124,7 +159,7 @@ impl App { .font(ui_font()) .color(Palette::TEXT_MUTED()); - // Favorite button — FontAwesome 6 Free, solid/regular variant for filled/empty. + // Favorite button - FontAwesome 6 Free, solid/regular variant for filled/empty. // Same codepoint \u{f005} in both variants; the weight picks the glyph. let is_fav = self.is_favorite(&repo.name); let fav_color = if is_fav { @@ -254,7 +289,7 @@ impl App { let current_platform = github::current_platform_key(); let action_row = if let Some(app_path) = installed_path { - // App is installed — show Launch + Update buttons, and uninstall trash button + // App is installed - show Launch + Update buttons, and uninstall trash button let launch_label = crate::i18n::t_fmt("launch", &[("name", &repo.manifest.name)]); let launch_btn = button(glyph_label("\u{f04b}", launch_label, 14, ui_font_medium())) .on_press_maybe(if self.is_downloading { @@ -334,7 +369,7 @@ impl App { .spacing(12) .align_y(iced::Alignment::Center) } else { - // Not installed — single download button for current platform + // Not installed - single download button for current platform let spacer: Element<'_, Message> = container(text("")).width(Fill).into(); if repo.manifest.release_files.contains_key(current_platform) { let repo_name = repo.name.clone(); @@ -423,7 +458,7 @@ impl App { .align_y(iced::Alignment::Center); // Wrap markdown in a Shrink-sized container so the outer scrollable - // sees the true content height (iced #2630 — markdown rich_text can + // sees the true content height (iced #2630 - markdown rich_text can // otherwise hide overflow from the parent). let desc_container = container(description) .width(Length::Shrink) @@ -454,10 +489,7 @@ impl App { { match self.release_notes.get(&repo.name) { Some((tag, blocks)) if tag == new_tag => { - let md_settings = markdown::Settings::with_text_size( - sz(13), - markdown::Style::from_palette(iced::Theme::Dark.palette()), - ); + let md_settings = markdown::Settings::with_text_size(sz(13), md_style()); let notes_title = text(crate::i18n::t_fmt("whats_new", &[("version", new_tag)])) .size(sz(14)) diff --git a/src/ui/github_panel.rs b/src/ui/github_panel.rs index 3ee2dbd..e7c4aaa 100644 --- a/src/ui/github_panel.rs +++ b/src/ui/github_panel.rs @@ -173,7 +173,7 @@ impl App { session .username .as_ref() - .map(|u| format!(" — {u}")) + .map(|u| format!(" ({u})")) .unwrap_or_default() ); let user_label: Element<'_, Message> = if self.is_fetching_repos { diff --git a/src/ui/markdown_blocks.rs b/src/ui/markdown_blocks.rs index 4ece29f..1c67af5 100644 --- a/src/ui/markdown_blocks.rs +++ b/src/ui/markdown_blocks.rs @@ -4,9 +4,9 @@ //! two constructs can be rendered with native Colony widgets instead of the //! default iced markdown output: //! -//! - **shields.io badges** — `[![alt](img.shields.io/badge/..)](link)` lines +//! - **shields.io badges**: `[![alt](img.shields.io/badge/..)](link)` lines //! become colored pills matching our palette (no network round-trip). -//! - **GFM pipe tables** — parsed line-based and rendered via iced's `table` +//! - **GFM pipe tables**: parsed line-based and rendered via iced's `table` //! widget directly, skipping iced markdown's internal horizontal //! `scrollable` that captures vertical wheel events. //! @@ -151,7 +151,7 @@ fn try_parse_badges(lines: &[&str]) -> Option<(Vec, usize)> { } } -/// Accept either `[![alt](img)](link)` or `![alt](img)` — returns a Badge +/// Accept either `[![alt](img)](link)` or `![alt](img)` - returns a Badge /// when the image URL is a shields.io `/badge/{label}-{value}-{color}`. fn parse_badge_line(line: &str) -> Option { // Find the `![` of the image. diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 644f540..e1f3cf5 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -217,8 +217,8 @@ impl App { let caches = self.section( typo, "caches", - "clear_caches", - "clear_caches_desc", + "settings_section_caches", + "settings_caches_desc", vec![action_button( typo, "\u{f1f8}", diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 6c961bc..8e22604 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -39,55 +39,37 @@ impl App { .id(crate::ui::tutorial::ID_SIDEBAR_CATS) .height(Length::Fill); - // GitHub button — Nerd Font icon (U+F09B) + // GitHub button: Nerd Font icon (U+F09B). Styled like a section entry, + // with the open menu as the selected state. The texts take the button's + // text colour, except the accent that marks a connected account while + // the menu is closed (the open state's accent fill would swallow it). let github_connected = matches!(self.github_state, GitHubState::Connected { .. }); - let github_icon_color = if github_connected { - Palette::ACCENT() - } else if self.show_github_menu { - Palette::TEXT_PRIMARY() - } else { - Palette::TEXT_DIM() + let show_menu = self.show_github_menu; + let connected_mark = |t: iced::widget::Text<'static>| { + if github_connected && !show_menu { + t.color(Palette::ACCENT()) + } else { + t + } }; let mut github_btn_content = row![ - text("\u{f09b}") - .size(sz(18)) - .font(fonts::glyph_font()) - .color(github_icon_color), - text("GitHub") - .size(sz(13)) - .font(ui_font()) - .color(github_icon_color), + connected_mark(text("\u{f09b}").size(sz(18)).font(fonts::glyph_font())), + text("GitHub").size(sz(13)).font(ui_font()), ] .spacing(10) .align_y(iced::Alignment::Center); if github_connected { - github_btn_content = github_btn_content.push( - text("\u{f00c}") - .size(sz(13)) - .font(fonts::glyph_font()) - .color(github_icon_color), - ); + github_btn_content = github_btn_content.push(connected_mark( + text("\u{f00c}").size(sz(13)).font(fonts::glyph_font()), + )); } - let show_menu = self.show_github_menu; let github_btn = button(github_btn_content) .on_press(Message::ToggleGitHubMenu) .padding([10, 14]) .width(Fill) - .style(move |_theme, _status| button::Style { - background: Some( - if show_menu { - Palette::BG_SELECTED() - } else { - iced::Color::TRANSPARENT - } - .into(), - ), - text_color: Palette::TEXT_PRIMARY(), - border: iced::Border::default().rounded(6), - ..Default::default() - }); + .style(widgets::selection_style(show_menu)); let rescan_label: Element<'_, Message> = if self.is_scanning { glyph_label("\u{f110}", crate::i18n::t("scanning"), 13, ui_font()).into() @@ -97,7 +79,18 @@ impl App { .font(ui_font()) .into() }; - let rescan_btn_base = button(rescan_label).padding([8, 16]).width(Fill); + let rescan_btn_base = + button(rescan_label) + .padding([8, 16]) + .width(Fill) + .style(|_theme, status| { + crate::ui::theme::action_button_style( + status, + Palette::BTN_DEFAULT(), + Palette::BTN_HOVER(), + Palette::BTN_PRESSED(), + ) + }); let rescan_btn = if self.is_scanning { rescan_btn_base } else { diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 54ffe28..f22eb8c 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -1,4 +1,4 @@ -//! Colony's theme — now shared with the rest of the ecosystem. +//! Colony's theme - now shared with the rest of the ecosystem. //! //! The 38-field palette, every theme palette, the `(family, variant)` //! resolver, the picker catalog, the accent overrides and the high-contrast @@ -79,7 +79,7 @@ mod doc_parity_tests { /// altogether. Adding a palette should fail here, not go unnoticed. /// /// This used to parse this file's own source for `=> ThemePalette::` arms. - /// The palettes now come from colony-ui, so it counts the catalog itself — + /// The palettes now come from colony-ui, so it counts the catalog itself - /// which is what the picker renders, and therefore what the docs describe. #[test] fn the_documented_theme_counts_match_the_code() { diff --git a/src/update/keyboard.rs b/src/update/keyboard.rs index 7f7da26..e82be8b 100644 --- a/src/update/keyboard.rs +++ b/src/update/keyboard.rs @@ -128,7 +128,7 @@ impl App { match target { Some(key) if key.starts_with("repo:") => { self.active_colony_repo = Some(key["repo:".len()..].to_string()); - // Refresh the (repo, tab) markdown cache — + // Refresh the (repo, tab) markdown cache - // the detail view reads cached blocks only. self.refresh_detail_markdown(); } diff --git a/src/update/mod.rs b/src/update/mod.rs index 9d4680b..e2e79a6 100644 --- a/src/update/mod.rs +++ b/src/update/mod.rs @@ -201,7 +201,7 @@ impl App { self.active_colony_repo = None; self.keyboard_cursor = None; // Dismiss any open overlay panel so the section change is - // actually visible — otherwise users stay stuck on the + // actually visible - otherwise users stay stuck on the // GitHub / Settings panel even though the underlying // filter just changed. self.show_github_menu = false; @@ -906,10 +906,10 @@ mod tests { fn search_matches_description_and_display_name() { let mut app = App::new_for_test(); app.colony_repo_list = vec![ - repo("Grape", "Lecteur musique en Rust"), + repo("Grape", "Music player in Rust"), repo("orCAL", "Calendar overlay"), ]; - app.search_query = "musique".into(); + app.search_query = "music".into(); let hits: Vec<&str> = app .filtered_colony_repos() .iter() diff --git a/src/update/store.rs b/src/update/store.rs index f66c99b..7fc473c 100644 --- a/src/update/store.rs +++ b/src/update/store.rs @@ -380,7 +380,7 @@ impl App { .collect(); if repos.is_empty() { - // Nothing to check — reset the guard (otherwise it stays true + // Nothing to check - reset the guard (otherwise it stays true // forever, blocking all later checks) and still run the // chained launcher self-update check. self.is_checking_updates = false;