- Rust 85.5%
- Python 10.6%
- JavaScript 2.9%
- Shell 1%
| .kilo | ||
| data | ||
| debian | ||
| docs | ||
| resources | ||
| scripts | ||
| src | ||
| tests | ||
| vendor/hanzi_lookup | ||
| .gitignore | ||
| AGENTS.md | ||
| cargo-sources.json | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| LICENSE-APL | ||
| LICENSE-FrequencyWords | ||
| LICENSE-LGPL-3.0.txt | ||
| LICENSE-rusqlite | ||
| LICENSE-UNICODE | ||
| LICENSE-unicode-normalization-MIT | ||
| org.spacecruft.HanziDraw.json | ||
| README.md | ||
| UPSTREAM.md | ||
Hanzi Draw Rust
A fully offline native GTK4 + libadwaita port of HanziDraw, targeting the Purism Librem 5 and distributed as a Flatpak. It provides handwriting candidates, single-character and single-syllable pinyin lookup, explicit English → Mandarin full-word lookup, CC-CEDICT definitions, and stroke-order animation.
The lookup selector defaults to Mandarin. Selecting English searches definitions and returns complete single- and multi-character Mandarin entries, including their raw pinyin and definitions. This does not add direct multi-Hanzi word input or multi-syllable-pinyin word search; both remain out of scope, and Mandarin keeps its existing single-character/single-syllable behavior. English search is lexical lookup, not machine translation.
The Svelte app under tmp/repos/hanzi-draw/ is a read-only reference clone,
not part of the native build or Flatpak. Its raw words.json is used only when
regenerating the English database and is not shipped.
Prerequisites
- Rust stable via rustup, >= 1.92 (PureOS crimson's rustc 1.63 is too old).
- For native builds, GTK >= 4.8 and libadwaita >= 1.2 development
files with
pkg-config. Crimson's GTK 4.8.3 and libadwaita 1.2.2 satisfy these requirements. - Node.js >= 20.19 only for the data/parity maintenance scripts.
- For Flatpak builds:
flatpak-builder,org.gnome.Platform//49,org.gnome.Sdk//49, andorg.freedesktop.Sdk.Extension.rust-stable. Regenerating Cargo sources also needs Python 3 andflatpak-cargo-generator.pyfrom flatpak-builder-tools.
See docs/PORT-rust.md for the platform constraints and rationale.
Native build and run
cargo build
glib-compile-schemas resources/
GSETTINGS_SCHEMA_DIR="$PWD/resources" cargo run
glib-compile-schemas generates resources/gschemas.compiled; rerun it after
changing the schema. If Debian does not expose the compiler on PATH, use its
multiarch path, such as
/usr/lib/x86_64-linux-gnu/glib-2.0/glib-compile-schemas on x86_64.
GSETTINGS_SCHEMA_DIR tells development runs where to find the compiled schema;
the Flatpak uses its installed system schema directory instead.
Without PKGDATADIR, stroke data loads from data/strokes-shards/ and the
English database loads from data/english-dictionary.sqlite in the checkout.
With PKGDATADIR, the corresponding packaged paths are
$PKGDATADIR/strokes-shards/ and $PKGDATADIR/english-dictionary.sqlite; the
Flatpak sets PKGDATADIR=/app/share/hanzi-draw.
The English database is a separate read-only file, not embedded in the executable. Startup and Mandarin lookup do not check or open it. The first English selection starts one per-window worker and opens the database asynchronously; subsequent mode changes reuse that bounded connection until the window closes. There is no runtime corpus parsing, index generation, or download. A missing or corrupt English database is an English-mode error and must not silently switch to pinyin results.
Native Debian package
PureOS crimson packaging uses a staged source tree, locked Cargo vendoring, and offline compilation. See docs/packaging.md for dependencies, payload details, release rules, and validation boundaries.
With Debian-packaged Rust/Cargo 1.92 or newer and the remaining build dependencies installed, build from the repository root:
sh scripts/build-native.sh
When rustup supplies the toolchain outside dpkg, use the explicit dependency- check bypass (all actual tools and libraries are still required):
sh scripts/build-native.sh -d
The exact .deb is written below build/native-source/. The application is
compiled with PKGDATADIR=/usr/share/hanzi-draw; only the committed English
SQLite database and 99 stroke shards are installed as external lookup data.
The package depends on fonts-noto-cjk and retains bundled SQLite/FTS5.
Inspect and test the candidate without installing it:
candidate=build/native-source/hanzi-draw_VERSION_ARCH.deb
lintian "$candidate"
PYTHONDONTWRITEBYTECODE=1 python3 tests/debian_package_smoke.py "$candidate"
Package builds, installed-package checks, and Librem 5 acceptance are separate validation stages recorded in docs/DEBIAN-VALIDATION.md.
Tests and maintenance data
cargo test
cargo test --features dictionary-generator
cargo test --manifest-path vendor/hanzi_lookup/Cargo.toml
cargo fmt --package hanzi-draw -- --check
cargo clippy --package hanzi-draw --all-targets --features dictionary-generator -- -D warnings
The feature-enabled test and clippy commands include the English database generator as well as the shared normalization/search code. The differential engine-parity test is ignored during normal runs. Run it after engine or parity-fixture changes (a full debug run takes about nine minutes):
cargo test -- --include-ignored
Regenerate-only maintenance data
Never hand-edit generated stroke shards, parity fixtures,
data/word-freq.json, or data/english-dictionary.sqlite. Regenerate them
only when their locked inputs or generators change, and keep UPSTREAM.md
provenance and the separate validation evidence current. Do not run generators
inside the reference clone.
With the read-only reference clone present, regenerate the older stroke and parity artifacts only when their inputs change:
node scripts/build-strokes-shards.mjs # upstream stroke data changed
node scripts/parity-expectations.mjs # JS baseline/parity corpus changed
English search uses two additional generated artifacts:
data/word-freq.jsoncontains derived whole-headword frequency counts. It is a generation-time input and is not installed in the Flatpak.data/english-dictionary.sqlitecontains the complete single- and multi-character entries plus derived English search indexes. It is committed and installed as the only English lookup runtime data file; rawwords.jsonandword-freq.jsonare not installed.
The FrequencyWords input has a new native PHASE 7 pin, not a claimed
reconstruction of the reference project's historical raw input. See
UPSTREAM.md for its revision, path, hash, and licensing. The
frequency generator's native cache is scripts/.cache/zh_cn_50k.txt, which is
gitignored and outside the reference clone. With no source argument, the script
uses a valid cached file or downloads the locked input as a generation-time-only
operation. It verifies the downloaded bytes' SHA-256, writes
scripts/.cache/zh_cn_50k.txt.part, and atomically renames that file before
use; neither the application nor the Flatpak performs this download:
node scripts/build-word-freq.mjs
To disallow network access during maintenance, supply an existing copy of the
locked input explicitly. The script verifies the same hash before deriving
data/word-freq.json:
node scripts/build-word-freq.mjs /path/to/zh_cn_50k.txt
After either frequency command, regenerate the database. This command reads
native data/cedict.json, generated data/word-freq.json, and
tmp/repos/hanzi-draw/public/data/words.json from the read-only reference:
cargo run --features dictionary-generator --bin build_english_dict
Use these same commands after changing the shared normalizer, schema, or
generator. Review the generator's actual counts, integrity checks, output hash,
and reproducibility evidence; do not infer them from the locked source totals.
Record final generation evidence in UPSTREAM.md and PHASE 7 validation rather
than treating a completed command as installed-Flatpak or device validation.
Flatpak build, run, and checks
cargo-sources.json is generated from the committed Cargo.lock; the in-tree
vendor/hanzi_lookup path dependency needs no separate source entry.
Regenerate it only when Cargo.lock changes, not for source-only edits
(adjust the generator script path for your installation):
python3 flatpak-cargo-generator.py Cargo.lock -o cargo-sources.json
Build and install for the current user, then run and inspect permissions:
flatpak-builder --user --install --force-clean build-dir org.spacecruft.HanziDraw.json
flatpak run org.spacecruft.HanziDraw
flatpak info --show-permissions org.spacecruft.HanziDraw
Confirm that Wayland/fallback-X11 and IPC are present and that no network
permission is granted. Check packaged shard and English-database loading and
GSettings theme persistence. The Flatpak installs only
/app/share/hanzi-draw/english-dictionary.sqlite for English search: it must
not contain raw words.json or word-freq.json, SQLite journal/WAL files,
Node, the generator, or reference-clone paths. Validate the metadata:
appstreamcli validate --explain resources/org.spacecruft.HanziDraw.metainfo.xml
desktop-file-validate resources/org.spacecruft.HanziDraw.desktop
For aarch64, run the same flatpak-builder command on the Librem 5; there is
no cross-compilation setup. Host and device checks have separate evidence
sections in the PHASE 6 validation report.
PHASE 7 has its own host, staged-Flatpak, and Librem 5 validation
report. Native and staged checks do not replace
installed-Flatpak or device evidence, and PHASE 7 work does not complete any
pending PHASE 6 row. These instructions do not assert that those checks have
passed.
Flathub submission is a separate user-owned PR. The manifest preserves the PHASE 6 locked local-build inputs and is not yet submission-ready; review the current Flathub follow-ups in the PHASE 6 validation report before preparing a submission.
License and provenance
The app is GPL-3.0-or-later. Bundled components retain their own licenses:
hanzi_lookup is LGPL-3.0, recognition/stroke data is under the Arphic
Public License, CC-CEDICT and adapted English dictionary/search data are
CC BY-SA 4.0, and FrequencyWords plus derived frequency data are MIT.
See UPSTREAM.md for source pins, the final generation record,
SQLite/normalization dependency notices, engine-equivalence parity
results, attribution and license links, reference-only assets, and the scoped
provenance review of the identified hanzidict-derived material.