diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9ef1365..8debb6d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,6 +24,9 @@ jobs:
with:
node-version: '22'
+ - name: Install jq
+ run: sudo apt-get install -y jq
+
- name: Clone kobopatch source
run: |
cd kobopatch-wasm
@@ -56,7 +59,7 @@ jobs:
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "run=true" >> "$GITHUB_OUTPUT"
- elif git diff --name-only HEAD~1 HEAD | grep -qE '^(kobopatch-wasm/|web/|tests/|nickelmenu/|koreader/)'; then
+ elif git diff --name-only HEAD~1 HEAD | grep -qE '^(kobopatch-wasm/|web/|tests/|nickelmenu/|koreader/|readerly/)'; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
@@ -82,6 +85,10 @@ jobs:
if: steps.check-e2e.outputs.run == 'true' && env.GITEA_ACTIONS != 'true'
run: koreader/setup.sh
+ - name: Set up Readerly assets
+ if: steps.check-e2e.outputs.run == 'true' && env.GITEA_ACTIONS != 'true'
+ run: readerly/setup.sh
+
- name: Full integration test (Playwright)
if: steps.check-e2e.outputs.run == 'true' && env.GITEA_ACTIONS != 'true'
run: |
diff --git a/.gitignore b/.gitignore
index 53a6e31..205195e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,8 +14,8 @@ kobopatch-wasm/wasm_exec.js
# Generated files in src (written by build scripts, regenerated on demand)
web/src/js/wasm_exec.js
web/src/nickelmenu/NickelMenu.zip
-web/src/nickelmenu/kobo-config.zip
web/src/koreader/
+web/src/readerly/
# Build output
web/dist/
diff --git a/README.md b/README.md
index b8bd8d3..71c4143 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,14 @@ A web application for customising Kobo e-readers. It supports two modes:
- These changes are wiped when system updates are released. Requires re-patching when system updates are installed.
- Gives you a lot of customization options, but not all of them may work correctly.
+## Prerequisites
+
+- [Node.js](https://nodejs.org/) (includes npm)
+- [jq](https://jqlang.github.io/jq/) — `brew install jq` / `apt install jq`
+- [Git](https://git-scm.com/)
+
+Go is required for the WASM build but downloaded automatically if not installed.
+
## How it works
The app uses the **Filesystem Access API** (Chromium) to interface with connected Kobo devices, or falls back to manual model/software version selection with a downloadable ZIP on other browsers.
@@ -44,7 +52,7 @@ web/
app.js # ES module entry point: step navigation, flow orchestration
kobo-device.js # KoboModels, KoboDevice class
kobo-software-urls.js # Fetches download URLs from JSON, getSoftwareUrl, getDevicesForVersion
- nickelmenu.js # NickelMenuInstaller: downloads/bundles NickelMenu + config
+ nickelmenu/ # NickelMenu feature modules + installer orchestrator
patch-ui.js # PatchUI: loads patches, parses YAML, renders toggle UI
patch-runner.js # KoboPatchRunner: spawns Web Worker per build
patch-worker.js # Web Worker: loads WASM, runs patchFirmware()
@@ -54,9 +62,8 @@ web/
index.json # Available patch manifest
downloads.json # Firmware download URLs by version/serial (may be auto-generated)
patches_*.zip # Patch files per firmware version
- nickelmenu/ # NickelMenu assets (generated by nickelmenu/setup.sh, gitignored)
- NickelMenu.zip
- kobo-config.zip
+ nickelmenu/ # NickelMenu assets (NickelMenu.zip generated by nickelmenu/setup.sh, gitignored)
+ readerly/ # Readerly font assets (generated by readerly/setup.sh, gitignored)
koreader/ # KOReader assets (generated by koreader/setup.sh, gitignored)
koreader-kobo.zip
release.json
@@ -64,12 +71,15 @@ web/
dist/ # Build output (gitignored, fully regenerable)
bundle.js # esbuild output (minified, content-hashed)
index.html # Generated with cache-busted references
- css/ favicon/ patches/ nickelmenu/ koreader/ wasm/ js/wasm_exec.js
+ css/ favicon/ patches/ nickelmenu/ readerly/ koreader/ wasm/ js/wasm_exec.js
build.mjs # esbuild build script + asset copy
package.json # esbuild, jszip
nickelmenu/
- setup.sh # Downloads NickelMenu.zip and bundles kobo-config.zip
+ setup.sh # Downloads NickelMenu.zip
+
+readerly/
+ setup.sh # Downloads latest Readerly fonts from GitHub releases
koreader/
setup.sh # Downloads latest KOReader release for Kobo
@@ -122,7 +132,15 @@ cd kobopatch-wasm
nickelmenu/setup.sh
```
-This downloads `NickelMenu.zip` and clones/updates the [kobo-config](https://github.com/nicoverbruggen/kobo-config) repo to bundle `kobo-config.zip` into `web/src/nickelmenu/`.
+This downloads `NickelMenu.zip` into `web/src/nickelmenu/`.
+
+## Setting up Readerly font assets
+
+```bash
+readerly/setup.sh
+```
+
+This downloads the latest [Readerly](https://github.com/nicoverbruggen/readerly) font release (`KF_Readerly.zip`) into `web/src/readerly/`. The fonts are served from the app's own domain and downloaded by the browser at install time.
## Setting up KOReader assets
@@ -159,7 +177,7 @@ npm run dev # dev server with watch mode on :8889
This serves the app at `http://localhost:8888`. The script automatically:
-1. Sets up NickelMenu assets if missing (`web/src/nickelmenu/`)
+1. Sets up NickelMenu, KOReader, and Readerly assets if missing
2. Builds the JS bundle (`web/dist/bundle.js`)
3. Builds the WASM binary if missing (`web/dist/wasm/kobopatch.wasm`)
diff --git a/koreader/setup.sh b/koreader/setup.sh
index 423667e..3620fd3 100755
--- a/koreader/setup.sh
+++ b/koreader/setup.sh
@@ -8,8 +8,13 @@ mkdir -p "$PUBLIC_DIR"
echo "Fetching latest KOReader release info..."
RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/koreader/koreader/releases/latest)
-VERSION=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])")
-DOWNLOAD_URL=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; assets=json.load(sys.stdin)['assets']; print(next(a['browser_download_url'] for a in assets if 'koreader-kobo-' in a['name'] and a['name'].endswith('.zip')))")
+VERSION=$(echo "$RELEASE_JSON" | jq -r '.tag_name')
+DOWNLOAD_URL=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | test("koreader-kobo-.*\\.zip$")) | .browser_download_url')
+
+if [ -z "$VERSION" ] || [ "$VERSION" = "null" ] || [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then
+ echo "Error: Could not find KOReader Kobo release"
+ exit 1
+fi
echo "Downloading KOReader $VERSION..."
curl -fL --progress-bar -o "$PUBLIC_DIR/koreader-kobo.zip" "$DOWNLOAD_URL"
diff --git a/nickelmenu/setup.sh b/nickelmenu/setup.sh
index 648ad07..22dca33 100755
--- a/nickelmenu/setup.sh
+++ b/nickelmenu/setup.sh
@@ -12,28 +12,5 @@ echo "Downloading NickelMenu.zip..."
curl -fSL -o "$PUBLIC_DIR/NickelMenu.zip" "$NICKELMENU_URL"
echo " -> $(du -h "$PUBLIC_DIR/NickelMenu.zip" | cut -f1)"
-# --- kobo-config ---
-KOBO_CONFIG_DIR="$SCRIPT_DIR/kobo-config"
-if [ -d "$KOBO_CONFIG_DIR" ]; then
- echo "Updating kobo-config..."
- cd "$KOBO_CONFIG_DIR"
- git pull
-else
- echo "Cloning kobo-config..."
- git clone https://github.com/nicoverbruggen/kobo-config.git "$KOBO_CONFIG_DIR"
-fi
-
-# Copy the relevant assets into a zip for the web app.
-# Includes: .adds/, .kobo/screensaver/, fonts/
-echo "Bundling kobo-config.zip..."
-cd "$KOBO_CONFIG_DIR"
-zip -r "$PUBLIC_DIR/kobo-config.zip" \
- .adds/ \
- .kobo/screensaver/ \
- fonts/ \
- -x "*.DS_Store"
-
-echo " -> $(du -h "$PUBLIC_DIR/kobo-config.zip" | cut -f1)"
-
echo ""
echo "Done. Assets written to: $PUBLIC_DIR"
diff --git a/nixpacks.toml b/nixpacks.toml
index 8c3cd58..1f75b0d 100644
--- a/nixpacks.toml
+++ b/nixpacks.toml
@@ -1,7 +1,7 @@
providers = ["node"]
[phases.setup]
-nixPkgs = ["git", "curl", "zip", "gnutar", "nginx", "nodejs", "gettext"]
+nixPkgs = ["git", "curl", "zip", "gnutar", "nginx", "nodejs", "gettext", "jq"]
paths = ["/usr/local/go/bin"]
[phases.build]
@@ -9,6 +9,8 @@ cmds = [
"cd kobopatch-wasm && bash setup.sh",
"cd kobopatch-wasm && bash build.sh",
"cd nickelmenu && bash setup.sh",
+ "cd koreader && bash setup.sh",
+ "cd readerly && bash setup.sh",
"cd web && npm install && npm run build",
]
diff --git a/readerly/setup.sh b/readerly/setup.sh
new file mode 100755
index 0000000..f54613e
--- /dev/null
+++ b/readerly/setup.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PUBLIC_DIR="$SCRIPT_DIR/../web/src/readerly"
+
+mkdir -p "$PUBLIC_DIR"
+
+# Get latest release download URL for KF_Readerly.zip
+echo "Fetching latest Readerly release..."
+DOWNLOAD_URL=$(curl -fsSL https://api.github.com/repos/nicoverbruggen/readerly/releases/latest \
+ | jq -r '.assets[] | select(.name == "KF_Readerly.zip") | .browser_download_url')
+
+if [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then
+ echo "Error: Could not find KF_Readerly.zip in latest release"
+ exit 1
+fi
+
+echo "Downloading KF_Readerly.zip..."
+curl -fSL -o "$PUBLIC_DIR/KF_Readerly.zip" "$DOWNLOAD_URL"
+echo " -> $(du -h "$PUBLIC_DIR/KF_Readerly.zip" | cut -f1)"
+
+echo ""
+echo "Done. Assets written to: $PUBLIC_DIR"
diff --git a/serve-locally.sh b/serve-locally.sh
index 2a3703a..c806338 100755
--- a/serve-locally.sh
+++ b/serve-locally.sh
@@ -22,6 +22,11 @@ if [ ! -f "$SRC_DIR/koreader/koreader-kobo.zip" ]; then
"$SCRIPT_DIR/koreader/setup.sh"
fi
+if [ ! -f "$SRC_DIR/readerly/KF_Readerly.zip" ]; then
+ echo "Readerly font assets not found, downloading..."
+ "$SCRIPT_DIR/readerly/setup.sh"
+fi
+
echo "Building JS bundle..."
cd "$WEB_DIR"
npm install --silent
diff --git a/tests/e2e/helpers/assets.js b/tests/e2e/helpers/assets.js
index 53fa713..68f5dcd 100644
--- a/tests/e2e/helpers/assets.js
+++ b/tests/e2e/helpers/assets.js
@@ -4,7 +4,7 @@ const { WEBROOT, WEBROOT_FIRMWARE, FIRMWARE_PATH } = require('./paths');
function hasNickelMenuAssets() {
return fs.existsSync(path.join(WEBROOT, 'nickelmenu', 'NickelMenu.zip'))
- && fs.existsSync(path.join(WEBROOT, 'nickelmenu', 'kobo-config.zip'));
+ && fs.existsSync(path.join(WEBROOT, 'nickelmenu', 'features', 'custom-menu', 'items'));
}
function hasKoreaderAssets() {
diff --git a/tests/e2e/integration.spec.js b/tests/e2e/integration.spec.js
index 201c02f..ac59a2d 100644
--- a/tests/e2e/integration.spec.js
+++ b/tests/e2e/integration.spec.js
@@ -39,7 +39,7 @@ test.describe('NickelMenu', () => {
await expect(page.locator('#nm-config-options')).not.toBeHidden();
// Verify default checkbox states
- await expect(page.locator('input[name="nm-cfg-fonts"]')).toBeChecked();
+ await expect(page.locator('input[name="nm-cfg-readerly-fonts"]')).toBeChecked();
await expect(page.locator('input[name="nm-cfg-screensaver"]')).not.toBeChecked();
await expect(page.locator('input[name="nm-cfg-simplify-tabs"]')).not.toBeChecked();
await expect(page.locator('input[name="nm-cfg-simplify-home"]')).not.toBeChecked();
@@ -256,8 +256,8 @@ test.describe('NickelMenu', () => {
await expect(page.locator('#step-nm-review')).not.toBeHidden();
await expect(page.locator('#nm-review-list')).toContainText('NickelMenu');
await expect(page.locator('#nm-review-list')).toContainText('Readerly fonts');
- await expect(page.locator('#nm-review-list')).toContainText('Simplified tab menu');
- await expect(page.locator('#nm-review-list')).toContainText('Simplified homescreen');
+ await expect(page.locator('#nm-review-list')).toContainText('Hide certain navigation tabs');
+ await expect(page.locator('#nm-review-list')).toContainText('Hide certain home screen elements');
// Both buttons visible when device is connected
await expect(page.locator('#btn-nm-write')).toBeVisible();
diff --git a/web/src/index.html b/web/src/index.html
index d1ae406..9e8707b 100644
--- a/web/src/index.html
+++ b/web/src/index.html
@@ -177,49 +177,7 @@
-
-
-
-
-
-
-
Learn more about these customisations ›
+