diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 963acbaf..00000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,21 +0,0 @@ -# Cross-compilation configuration for pixelsrc/pxl - -# Linux x86_64 (GNU) -[target.x86_64-unknown-linux-gnu] -linker = "x86_64-linux-gnu-gcc" - -# Linux ARM64 (GNU) -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" - -# macOS x86_64 -[target.x86_64-apple-darwin] -linker = "clang" - -# macOS ARM64 (Apple Silicon) -[target.aarch64-apple-darwin] -linker = "clang" - -# Windows x86_64 (MSVC) -[target.x86_64-pc-windows-msvc] -# Uses default MSVC linker when available diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml index be860870..024d01dd 100644 --- a/.github/workflows/browser-tests.yml +++ b/.github/workflows/browser-tests.yml @@ -31,9 +31,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - cache: 'npm' - cache-dependency-path: | - wasm/package-lock.json - name: Setup Rust uses: dtolnay/rust-toolchain@stable @@ -77,9 +74,8 @@ jobs: test.beforeAll(async () => { // Start a simple HTTP server - server = spawn('npx', ['serve', '-p', PORT, 'docs/book/book'], { - stdio: 'pipe', - shell: true + server = spawn('python3', ['-m', 'http.server', String(PORT), '--directory', 'docs/book/book/html'], { + stdio: 'pipe' }); // Wait for server to start await new Promise(resolve => setTimeout(resolve, 3000)); @@ -106,6 +102,8 @@ jobs: test('search functionality works', async ({ page }) => { await page.goto(BASE_URL); // Check search is available + // mdBook keeps the search bar hidden until search is opened + await page.locator('#search-toggle').click(); const searchInput = page.locator('#searchbar'); await expect(searchInput).toBeVisible(); }); diff --git a/.github/workflows/crates.yml b/.github/workflows/crates.yml index f7d5d93b..3326aa1e 100644 --- a/.github/workflows/crates.yml +++ b/.github/workflows/crates.yml @@ -3,6 +3,13 @@ name: Publish to crates.io on: release: types: [published] + # Called by release-plz.yml after it tags a release (a GITHUB_TOKEN release + # cannot trigger the `release` arm above). + workflow_call: + inputs: + tag: + required: true + type: string workflow_dispatch: inputs: dry_run: @@ -23,6 +30,8 @@ jobs: id-token: write # Required for OIDC trusted publishing steps: - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag || github.ref }} - name: Setup Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index 7b389c8d..e19008b5 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -3,6 +3,16 @@ name: Update Homebrew Formula on: release: types: [published] + # Called by release-plz.yml once the binaries are attached (a GITHUB_TOKEN + # release cannot trigger the `release` arm above). + workflow_call: + inputs: + tag: + required: true + type: string + secrets: + HOMEBREW_TAP_DEPLOY_KEY: + required: true workflow_dispatch: inputs: version: @@ -10,25 +20,34 @@ on: required: true type: string +# The formula lives in the tap repo, scbrown/homebrew-pixelsrc, so that +# `brew install scbrown/pixelsrc/pxl` works. CI writes to it with a deploy key +# (HOMEBREW_TAP_DEPLOY_KEY) scoped to that one repo. env: - FORMULA_FILE: homebrew-pixelsrc/Formula/pxl.rb + FORMULA_FILE: tap/Formula/pxl.rb + # the working directory is not a pixelsrc checkout, so tell gh which repo + GH_REPO: ${{ github.repository }} jobs: update-formula: name: Update Homebrew formula runs-on: ubuntu-latest permissions: - contents: write + contents: read steps: - - name: Checkout + - name: Checkout the tap uses: actions/checkout@v4 with: - ref: main + repository: scbrown/homebrew-pixelsrc + ssh-key: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }} + path: tap - name: Get version id: version run: | - if [ -n "${{ inputs.version }}" ]; then + if [ -n "${{ inputs.tag }}" ]; then + VERSION="${{ inputs.tag }}" + elif [ -n "${{ inputs.version }}" ]; then VERSION="${{ inputs.version }}" else VERSION="${{ github.event.release.tag_name }}" @@ -88,7 +107,7 @@ jobs: # frozen_string_literal: true # Homebrew formula for pxl - Pixelsrc CLI - # Install: brew tap scbrown/pixelsrc && brew install pxl + # Install: brew install scbrown/pixelsrc/pxl class Pxl < Formula desc "GenAI-native pixel art format and compiler" homepage "https://github.com/scbrown/pixelsrc" @@ -131,9 +150,10 @@ jobs: cat $FORMULA_FILE - name: Commit and push + working-directory: tap run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add $FORMULA_FILE - git diff --staged --quiet || git commit -m "Update Homebrew formula for ${{ steps.version.outputs.tag }}" + git add Formula/pxl.rb + git diff --staged --quiet || git commit -m "pxl ${{ steps.version.outputs.number }}" git push diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 9fb5edc7..3a6b113d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -10,6 +10,9 @@ on: env: CARGO_TERM_COLOR: always + # pyo3 0.23 tops out at Python 3.13; runners now carry 3.14, which + # --find-interpreter picks up. Build those against the stable ABI. + PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1" jobs: linux: @@ -25,7 +28,9 @@ jobs: - uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist + # PYO3_USE_ABI3_FORWARD_COMPATIBILITY does not reach the manylinux + # container, so name the interpreters pyo3 0.23 supports. + args: --release --out dist -i python3.9 python3.10 python3.11 python3.12 python3.13 manylinux: auto working-directory: python @@ -41,7 +46,7 @@ jobs: fail-fast: false matrix: include: - - runner: macos-13 + - runner: macos-15-intel target: x86_64-apple-darwin - runner: macos-14 target: aarch64-apple-darwin diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 00000000..fae2a395 --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,69 @@ +name: Release-plz + +# On every push to main: open or update the release PR (version bump + +# CHANGELOG). When that PR merges: tag v, create the GitHub release, +# then build the binaries, update the Homebrew tap and publish to crates.io +# from this same run. +on: + push: + branches: [main] + +concurrency: + group: release-plz + cancel-in-progress: false + +jobs: + release-plz: + name: Release-plz + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + outputs: + releases_created: ${{ steps.release.outputs.releases_created }} + tag: ${{ steps.tag.outputs.tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - uses: MarcoIeni/release-plz-action@v0.5 + id: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Resolve the new tag + id: tag + if: steps.release.outputs.releases_created == 'true' + env: + RELEASES: ${{ steps.release.outputs.releases }} + run: echo "tag=$(echo "$RELEASES" | jq -r '.[0].tag')" >> "$GITHUB_OUTPUT" + + binaries: + name: Binaries + needs: release-plz + if: needs.release-plz.outputs.releases_created == 'true' + permissions: + contents: write + uses: ./.github/workflows/release.yml + with: + tag: ${{ needs.release-plz.outputs.tag }} + + homebrew: + name: Homebrew tap + needs: [release-plz, binaries] + uses: ./.github/workflows/homebrew.yml + with: + tag: ${{ needs.release-plz.outputs.tag }} + secrets: + HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }} + + crates: + name: crates.io + needs: release-plz + if: needs.release-plz.outputs.releases_created == 'true' + permissions: + contents: read + id-token: write + uses: ./.github/workflows/crates.yml + with: + tag: ${{ needs.release-plz.outputs.tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b47eaedc..cf0dd804 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,13 @@ on: push: tags: - 'v*' + # Called by release-plz.yml after it tags a release (a GITHUB_TOKEN tag push + # cannot trigger the `tags` arm above). + workflow_call: + inputs: + tag: + required: true + type: string workflow_dispatch: inputs: version: @@ -13,7 +20,7 @@ on: env: CARGO_TERM_COLOR: always - VERSION: ${{ inputs.version || github.ref_name }} + VERSION: ${{ inputs.tag || inputs.version || github.ref_name }} jobs: build: @@ -49,6 +56,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ env.VERSION }} - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -97,6 +106,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ env.VERSION }} - name: Download Linux x86_64 binary uses: actions/download-artifact@v4 @@ -194,5 +205,6 @@ jobs: with: tag_name: ${{ env.VERSION }} files: artifacts/* - generate_release_notes: true + # release-plz already wrote the notes from the changelog; keep them. + generate_release_notes: ${{ inputs.tag == '' }} fail_on_unmatched_files: true diff --git a/README.md b/README.md index 229de4ed..02d450e9 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ [![CI](https://github.com/scbrown/pixelsrc/actions/workflows/ci.yml/badge.svg)](https://github.com/scbrown/pixelsrc/actions/workflows/ci.yml) [![Release](https://github.com/scbrown/pixelsrc/actions/workflows/release.yml/badge.svg)](https://github.com/scbrown/pixelsrc/actions/workflows/release.yml) [![WASM](https://github.com/scbrown/pixelsrc/actions/workflows/wasm.yml/badge.svg)](https://github.com/scbrown/pixelsrc/actions/workflows/wasm.yml) -[![Version](https://img.shields.io/crates/v/pxl)](https://crates.io/crates/pxl) +[![Version](https://img.shields.io/crates/v/pixelsrc)](https://crates.io/crates/pixelsrc) [![npm](https://img.shields.io/npm/v/@stiwi/pixelsrc-wasm)](https://www.npmjs.com/package/@stiwi/pixelsrc-wasm) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Made with pixelsrc](https://img.shields.io/badge/made%20with-pixelsrc-BD93F9)](https://pixelsrc.dev) +[![Made with pixelsrc](https://img.shields.io/badge/made%20with-pixelsrc-BD93F9)](https://scbrown.github.io/pixelsrc/) **The first pixel art format designed for GenAI.** @@ -54,12 +54,14 @@ pixelsrc is a semantic, human-readable text format for defining pixel art. Unlik ### Homebrew (macOS/Linux) ```bash -brew install scbrown/tap/pixelsrc +brew install scbrown/pixelsrc/pxl ``` -### Cargo (from source) +### Cargo ```bash +cargo install pixelsrc +# or the latest main: cargo install --git https://github.com/scbrown/pixelsrc ``` diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..35fd6ffb --- /dev/null +++ b/cliff.toml @@ -0,0 +1,72 @@ +[changelog] +header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n" +footer = "" +body = """ +{%- macro remote_url() -%} + https://github.com/scbrown/pixelsrc +{%- endmacro -%} + +{% if version -%} +## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else -%} +## [Unreleased] +{% endif -%} + +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | striptags | trim | upper_first }} +{% for commit in commits %} +- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ +{% if commit.breaking %}[**breaking**] {% endif %}\ +{{ commit.message | upper_first }}\ +([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ +{% endfor %} +{% endfor %} +""" +# Preserve the body's trailing newline so adjacent release sections keep a +# blank line between the final list item and the next version heading. +trim = false + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +protect_breaking_commits = false +filter_commits = false +tag_pattern = "v[0-9].*" +sort_commits = "oldest" + +# Strip internal ticket IDs from PUBLISHED changelog entries. Commit messages +# keep the refs (internal git history wants them); the public CHANGELOG must +# not carry them — same enforcement split as the pre-push guard, and the same +# preprocessor block quipu's cliff.toml carries. Without this, an entry like +# "... pre-edit guard ()" ships verbatim (it was about to). +commit_preprocessors = [ + # Keep a leading tracker-style subject as a release entry. + { pattern = '^(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*:[ \t]*', replace = "other: " }, + # "(ID, rest)" -> "(rest)" e.g. (proj-1a2b, #36/#37) -> (#36/#37) + { pattern = '\((?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*,[ \t]*', replace = "(" }, + # "(rest, ID)" -> "(rest)" + { pattern = ',[ \t]*(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\)', replace = ")" }, + # " (ID)" standalone paren -> removed e.g. ... shape (proj-1a2b) -> ... shape + { pattern = '[ \t]*\((?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\)', replace = "" }, + # a RUN of bracketed refs -> removed e.g. ... dirs [proj-1a2b] [proj-3c4d] + { pattern = '(?:[ \t]*\[(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\])+', replace = "" }, + # "[ID, rest]" -> "[rest]" and "[rest, ID]" -> "[rest]", mirroring the parens + { pattern = '\[(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*,[ \t]*', replace = "[" }, + { pattern = ',[ \t]*(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\]', replace = "]" }, + # any bare leftover " ID" + { pattern = '[ \t]*\b(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\b', replace = "" }, +] + +commit_parsers = [ + { message = "^other", group = "Other" }, + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^refactor", group = "Changed" }, + { message = "^doc", group = "Documentation" }, + { message = "^test", group = "Testing" }, + { message = "^chore", group = "Miscellaneous" }, + { message = "^ci", group = "CI/CD" }, + { body = ".*security", group = "Security" }, + { message = "^revert", group = "Reverted" }, +] diff --git a/docs/book/custom/js/wasm-demo.js b/docs/book/custom/js/wasm-demo.js index 48ec628d..238acb01 100644 --- a/docs/book/custom/js/wasm-demo.js +++ b/docs/book/custom/js/wasm-demo.js @@ -18,8 +18,10 @@ wasm: typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function', dynamicImport: (function() { try { - // Check if dynamic import is supported - return typeof import === 'function' || true; // Modern browsers + // Check if dynamic import is supported. `typeof import` is a + // SyntaxError that aborts this whole script, so probe via Function. + new Function('return import("data:text/javascript,")'); + return true; } catch (e) { return false; } diff --git a/homebrew-pixelsrc/Formula/pxl-src.rb b/homebrew-pixelsrc/Formula/pxl-src.rb deleted file mode 100644 index a0de66f6..00000000 --- a/homebrew-pixelsrc/Formula/pxl-src.rb +++ /dev/null @@ -1,23 +0,0 @@ -# typed: false -# frozen_string_literal: true - -# Homebrew formula for pxl - build from source -# Install: brew tap scbrown/pixelsrc && brew install pxl-src -class PxlSrc < Formula - desc "GenAI-native pixel art format and compiler (build from source)" - homepage "https://github.com/scbrown/pixelsrc" - url "https://github.com/scbrown/pixelsrc/archive/refs/tags/v0.1.0.tar.gz" - sha256 "PLACEHOLDER_SHA256_SOURCE" - license "MIT" - head "https://github.com/scbrown/pixelsrc.git", branch: "main" - - depends_on "rust" => :build - - def install - system "cargo", "install", *std_cargo_args - end - - test do - assert_match version.to_s, shell_output("#{bin}/pxl --version") - end -end diff --git a/homebrew-pixelsrc/Formula/pxl.rb b/homebrew-pixelsrc/Formula/pxl.rb deleted file mode 100644 index 725e1826..00000000 --- a/homebrew-pixelsrc/Formula/pxl.rb +++ /dev/null @@ -1,41 +0,0 @@ -# typed: false -# frozen_string_literal: true - -# Homebrew formula for pxl - Pixelsrc CLI -# Install: brew tap scbrown/pixelsrc && brew install pxl -class Pxl < Formula - desc "GenAI-native pixel art format and compiler" - homepage "https://github.com/scbrown/pixelsrc" - version "0.2.0" - license "MIT" - - on_macos do - on_arm do - url "https://github.com/scbrown/pixelsrc/releases/download/v0.2.0/pxl-v0.2.0-aarch64-apple-darwin.tar.gz" - sha256 "243cb8a3fddb10ae73c76e9a223ee91fab350d1c76e2e1a6a53d73dd8c5e153b" - end - on_intel do - url "https://github.com/scbrown/pixelsrc/releases/download/v0.2.0/pxl-v0.2.0-x86_64-apple-darwin.tar.gz" - sha256 "d6f6e559717337b11585a6193f28b2d12afdae46a377f4d0e0385314a053f880" - end - end - - on_linux do - on_arm do - url "https://github.com/scbrown/pixelsrc/releases/download/v0.2.0/pxl-v0.2.0-aarch64-unknown-linux-gnu.tar.gz" - sha256 "102572e116fae5e5fcdb2bb75c964eda1d46a66e8304bf34a45d4c3b412cc4d2" - end - on_intel do - url "https://github.com/scbrown/pixelsrc/releases/download/v0.2.0/pxl-v0.2.0-x86_64-unknown-linux-gnu.tar.gz" - sha256 "d376a081598379b105d11a36ef531eaedbb788c447311a643f4a1c190e032893" - end - end - - def install - bin.install "pxl" - end - - test do - assert_match version.to_s, shell_output("#{bin}/pxl --version") - end -end diff --git a/homebrew-pixelsrc/README.md b/homebrew-pixelsrc/README.md deleted file mode 100644 index 0b7fe4df..00000000 --- a/homebrew-pixelsrc/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Homebrew Tap for Pixelsrc - -This tap contains the Homebrew formula for `pxl`, the Pixelsrc CLI. - -## Installation - -```bash -# Add the tap -brew tap stiwi/pixelsrc - -# Install pxl -brew install pxl -``` - -## Available Formulas - -### pxl (binary) - -Pre-built binary for fast installation: - -```bash -brew install pxl -``` - -Supports: -- macOS (Intel and Apple Silicon) -- Linux (x86_64 and ARM64) - -### pxl-src (build from source) - -Build from source (requires Rust): - -```bash -brew install pxl-src -``` - -## Updating - -```bash -brew update -brew upgrade pxl -``` - -## Verification - -```bash -pxl --version -``` - -## About Pixelsrc - -Pixelsrc is a GenAI-native pixel art format and compiler. See the main repository for documentation. diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 00000000..66648aef --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,18 @@ +[workspace] +changelog_config = "cliff.toml" +# release-plz only cuts the release PR, the tag and the GitHub release. The +# binaries and the crates.io publish run as jobs in release-plz.yml, because a +# tag or release created with GITHUB_TOKEN does not trigger `on: push: tags` +# or `on: release` workflows. Left to those triggers, a release would ship +# with no binaries. +publish = false +# Use the tags as the baseline instead of asking crates.io; with publish off, +# the registry is not the source of truth. +git_only = true +git_release_enable = true +git_tag_enable = true +# The default template is `{{ package }}-v{{ version }}`; every tag here is v*. +git_tag_name = "v{{ version }}" + +[[package]] +name = "pixelsrc" diff --git a/src/analyze/dimensions.rs b/src/analyze/dimensions.rs index 447d1585..0ba1e45e 100644 --- a/src/analyze/dimensions.rs +++ b/src/analyze/dimensions.rs @@ -23,7 +23,7 @@ impl DimensionStats { /// Get dimension counts sorted by frequency. pub fn sorted_by_frequency(&self) -> Vec<((u32, u32), usize)> { let mut items: Vec<_> = self.dimensions.iter().map(|(k, v)| (*k, *v)).collect(); - items.sort_by(|a, b| b.1.cmp(&a.1)); + items.sort_by_key(|b| std::cmp::Reverse(b.1)); items } diff --git a/src/analyze/families.rs b/src/analyze/families.rs index 26af50a0..05bcf783 100644 --- a/src/analyze/families.rs +++ b/src/analyze/families.rs @@ -57,7 +57,7 @@ impl TokenFamilyDetector { .collect(); // Sort by total count descending - families.sort_by(|a, b| b.total_count.cmp(&a.total_count)); + families.sort_by_key(|f| std::cmp::Reverse(f.total_count)); families } diff --git a/src/analyze/tokens.rs b/src/analyze/tokens.rs index 2956e84c..3e84376d 100644 --- a/src/analyze/tokens.rs +++ b/src/analyze/tokens.rs @@ -111,7 +111,7 @@ impl CoOccurrenceMatrix { /// Get top N token pairs by co-occurrence count. pub fn top_n(&self, n: usize) -> Vec<((&String, &String), usize)> { let mut items: Vec<_> = self.pairs.iter().map(|((a, b), count)| ((a, b), *count)).collect(); - items.sort_by(|a, b| b.1.cmp(&a.1)); + items.sort_by_key(|b| std::cmp::Reverse(b.1)); items.truncate(n); items } @@ -131,7 +131,7 @@ impl CoOccurrenceMatrix { } }) .collect(); - results.sort_by(|a, b| b.1.cmp(&a.1)); + results.sort_by_key(|b| std::cmp::Reverse(b.1)); results } diff --git a/src/atlas.rs b/src/atlas.rs index cd20d616..c1ea00ed 100644 --- a/src/atlas.rs +++ b/src/atlas.rs @@ -129,7 +129,7 @@ pub fn pack_atlas( // Sort sprites by height descending (better packing) let mut sorted_sprites: Vec<(usize, &SpriteInput)> = sprites.iter().enumerate().collect(); - sorted_sprites.sort_by(|a, b| b.1.image.height().cmp(&a.1.image.height())); + sorted_sprites.sort_by_key(|s| std::cmp::Reverse(s.1.image.height())); let mut atlases: Vec<(RgbaImage, AtlasMetadata, Vec)> = vec![]; let mut sprite_to_atlas: HashMap = HashMap::new(); diff --git a/src/build/pipeline.rs b/src/build/pipeline.rs index 5762b1e6..b0b5da0c 100644 --- a/src/build/pipeline.rs +++ b/src/build/pipeline.rs @@ -593,10 +593,7 @@ impl BuildPipeline { // Select registries with import support let has_file_imports = !file_imports.is_empty(); - let effective_palette_reg; - let effective_sprite_reg; - - if has_file_imports { + let (effective_palette_reg, effective_sprite_reg) = if has_file_imports { if let Some(pr) = project_registry { for palette_name in pr.palette_names() { if let Some(loc) = pr.palette_location(palette_name) { @@ -613,20 +610,12 @@ impl BuildPipeline { local_sprite_registry.register_sprite(sprite.clone()); } } - effective_palette_reg = local_palette_registry; - effective_sprite_reg = local_sprite_registry; + (local_palette_registry, local_sprite_registry) + } else if let Some(pr) = project_registry { + (pr.palettes.clone(), pr.sprites.clone()) } else { - effective_palette_reg = if let Some(pr) = project_registry { - pr.palettes.clone() - } else { - local_palette_registry - }; - effective_sprite_reg = if let Some(pr) = project_registry { - pr.sprites.clone() - } else { - local_sprite_registry - }; - } + (local_palette_registry, local_sprite_registry) + }; let palette_registry = &effective_palette_reg; let sprite_registry = &effective_sprite_reg; diff --git a/src/composition/render.rs b/src/composition/render.rs index 1f696bb8..f82223b7 100644 --- a/src/composition/render.rs +++ b/src/composition/render.rs @@ -147,8 +147,8 @@ pub fn render_composition( } // Calculate expected grid dimensions for map validation - let expected_cols = if cell_size[0] > 0 { width / cell_size[0] } else { width }; - let expected_rows = if cell_size[1] > 0 { height / cell_size[1] } else { height }; + let expected_cols = width.checked_div(cell_size[0]).unwrap_or(width); + let expected_rows = height.checked_div(cell_size[1]).unwrap_or(height); // Create canvas (transparent by default) let mut canvas = RgbaImage::from_pixel(width, height, Rgba([0, 0, 0, 0])); diff --git a/src/install.rs b/src/install.rs index 60bc9c83..937c8d84 100644 --- a/src/install.rs +++ b/src/install.rs @@ -111,6 +111,10 @@ impl LockFile { } else { project_root.join(declared_path) }; + // install_path_dep records the canonical path, so compare against that: + // otherwise any symlink in the project path (macOS /var -> /private/var) + // makes every path dep look stale and reinstall on each run. + let resolved = resolved.canonicalize().unwrap_or(resolved); let resolved_str = resolved.to_string_lossy().to_string(); locked.dep_type == "path" && locked.source == resolved_str } else if dep.is_git() { diff --git a/src/lsp/server.rs b/src/lsp/server.rs index fd192490..ccfeadb6 100644 --- a/src/lsp/server.rs +++ b/src/lsp/server.rs @@ -189,10 +189,12 @@ impl PixelsrcLanguageServer { // Likely inside a region definition return CompletionContext::RegionDef; } - Some("palette") if line.contains(':') => { - if content.contains("\"roles\"") && !line.contains("\"colors\"") { - return CompletionContext::Roles; - } + Some("palette") + if line.contains(':') + && content.contains("\"roles\"") + && !line.contains("\"colors\"") => + { + return CompletionContext::Roles; } _ => {} }