Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm run validate-xovi
- run: pnpm lint
- run: pnpm test
- run: pnpm build
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public/templates/custom/
# Methods templates pulled from device
public/templates/methods/

# Server data (device config, SSH keys)
# Server data (device config, SSH keys — but not bundled xovi extensions)
data/
!**/server/data/
server/data/*
!server/data/xovi-extensions/

# Server build output
dist-server/
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ RUN pnpm install --frozen-lockfile
# Copy source
COPY . .

# Validate xovi extension checksums before building
RUN pnpm run validate-xovi

# Build frontend and server
RUN pnpm build

Expand Down
8 changes: 8 additions & 0 deletions docs/device-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,14 @@ For programmatic use: `POST /api/restore?mode=merge` (or `mode=replace` to overw

---

## xovi extensions

If you have [xovi](https://github.com/asivery/xovi) installed on your device, you can deploy curated UI extensions that enhance the template experience — unlocking Methods templates without a Connect subscription, normalizing page dimensions across device families, and improving quicksheet behavior. Extensions are managed from the **xovi Extensions** card on the **Device & Sync** page.

See the [xovi extensions guide](xovi-extensions.md) for the full list of available extensions, deployment instructions, and troubleshooting.

---

## Caveats

> [!WARNING]
Expand Down
12 changes: 10 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,23 @@ Each deploy:

> **Native vs PDF templates:** This app creates native `.template` files — vector-based pages that render instantly, use minimal battery, and zoom infinitely. PDF templates have their advantages (inter-page links, complex layouts) but are rasterized at fixed resolution and use more memory.

## 7. Back up your templates
## 7. Enhance with xovi extensions (optional)

If you've installed [xovi](https://github.com/asivery/xovi) on your device, you can deploy curated UI extensions directly from the **Device & Sync** page. These extensions unlock Methods templates without a Connect subscription, normalize page sizes across devices, and improve quicksheet behavior.

Scroll to the **xovi Extensions** card, click **Check xovi Status**, select your extensions, and deploy. See the [xovi extensions guide](xovi-extensions.md) for full details.

> **Don't have xovi?** Everything else in this app works without it. xovi is only needed if you want the optional UI extensions described above.

## 8. Back up your templates

Click **↓ Backup** on the **Device & Sync** page to download a ZIP of all your custom templates. This preserves UUIDs needed for device sync continuity.

To restore: click **↑ Restore** and select the backup ZIP. A preview shows what will be added, what's already present, and any local templates not in the backup that you can optionally clean up:

![Backup restore preview](images/backup-restore-preview.png)

## 8. Rollback
## 9. Rollback

If something goes wrong, use the **Device & Sync** page to roll back:

Expand Down
169 changes: 169 additions & 0 deletions docs/xovi-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# xovi Extensions

Enhance your reMarkable experience with curated xovi extensions, deployed directly from the **Device & Sync** page.

## What are xovi extensions?

[xovi](https://github.com/asivery/xovi) is a community framework that lets you tweak the reMarkable UI without permanently modifying system files. It works by intercepting the Qt resource system at startup and applying small patch files (`.qmd`) that modify UI behavior.

This app deploys a curated set of extensions that enhance the template experience — unlocking Methods templates without a subscription, normalizing page dimensions across devices, and improving quicksheet behavior.

> If the [Vellum package manager](https://remarkable.guide/guide/software/vellum.html) is installed on your device, you can install xovi directly from this app. Otherwise, install Vellum first, then use the Install button.

## Prerequisites

1. **[Vellum](https://remarkable.guide/guide/software/vellum.html)** installed on your device (see the install guide for bootstrap instructions)
2. **xovi** — install it one of two ways:
- **From this app:** Click **Check xovi Status**, then click **Install xovi** (requires Vellum on the device)
- **Via SSH:** `vellum add qt-resource-rebuilder` (pulls in xovi + xovi-extensions as dependencies)
3. A configured device connection in this app (see [quickstart](quickstart.md))

## Important: warranty & risks

- Modifying your device's software behavior **may void your warranty**
- Extensions are community-maintained and **not endorsed by reMarkable**
- All changes are **fully reversible** — remove the extensions and restart to restore stock behavior
- You accept responsibility for modifications to your device
- Extensions may need to be re-deployed after firmware updates

## Available extensions

### Unlock Methods Content (Essential)

Bypasses the subscription check for using on-device Methods templates and documents. Without this extension, Methods templates (including any custom templates you deploy via rm_methods format) require an active Connect subscription.

- **Works on:** reMarkable 1, reMarkable 2, Paper Pro, Paper Pro Move
- **Why you need it:** If you deploy custom templates via this app and don't have a Connect subscription, this extension ensures you can actually use them

### Page Size Normalization (Essential — pick one)

The three reMarkable device families have different screen dimensions:

| Device | Resolution |
|--------|-----------|
| reMarkable 1 & 2 | 1404 x 1872 |
| Paper Pro | 1620 x 2160 |
| Paper Pro Move | 954 x 1696 |

When you create a new page on one device, it's stamped with that device's dimensions. If you sync that page to a different device, it may appear zoomed in, zoomed out, or cropped.

These extensions force new pages to use a consistent size regardless of which device you're on:

| Your primary device | Install this | Why |
|---|---|---|
| reMarkable 1/2 | **Paper Pro Size** | Pages you create will render correctly on Paper Pro |
| Paper Pro | **RM2 Size** | Pages you create will render correctly on RM1/RM2 |
| Paper Pro Move | **Paper Pro Size** | Pages you create will render correctly on Paper Pro (most common sync target) |

**Important:** These extensions only affect **new pages** you create after installation. They do not retroactively change existing pages or synced content. You can only install one of the two — they are mutually exclusive.

### Prevent Notebook Zoom Out (Essential for Move)

Forces all notebook pages to start at 1x zoom, preventing the default zoom-out behavior. This is especially important on the Paper Pro Move, where pages created on larger devices would otherwise display zoomed out.

- **Best paired with** a page size extension for full cross-device consistency
- **Works on:** All devices, but designed primarily for Paper Pro Move

### Quicksheet Use Template (Recommended)

When you add a quicksheet page (quick-add at the bottom of a notebook), it normally uses the default blank template. This extension makes quicksheet pages inherit the template from the previous page — so if you're writing on a dot grid, your new page will also be a dot grid.

## How to deploy

1. Navigate to the **Device & Sync** page
2. Scroll to the **xovi Extensions** card
3. Click **Check xovi Status** to verify xovi is detected on your device
4. Select the extensions you want:
- Check/uncheck individual extensions
- Choose a page size option (RM2 Size, Paper Pro Size, or None)
5. Click **Deploy Selected**
6. Wait for the progress indicator to complete — the device UI will restart automatically

## How to remove

1. On the **xovi Extensions** card, click **Check xovi Status**
2. Click **Remove All Extensions**
3. Confirm the removal
4. The device UI restarts with stock behavior restored

To remove individual extensions, uncheck them and deploy again — only checked extensions will be present after deployment.

## After a firmware update

When your reMarkable receives a firmware update:

1. Extensions may stop working because the UI code they patch has changed
2. Open the app and click **Check xovi Status** to see the current state
3. If the new firmware version is supported, click **Deploy Selected** to re-deploy
4. If the new firmware version is not yet supported, you'll see a message — new versions are typically supported within a few days by the community

## Troubleshooting

### "xovi not installed"

xovi and qt-resource-rebuilder must be installed on your device before this app can deploy extensions. If Vellum is on your device, click **Install xovi** in the app. Otherwise, install via SSH:

```bash
# On your device (via SSH):
vellum add qt-resource-rebuilder
```

### "Vellum needs to be re-enabled"

After a firmware update, Vellum needs to re-apply its system modifications. The app will show a warning when this is detected. SSH into your device and run:

```bash
vellum reenable
```

Then check xovi status again in the app. Until reenable completes, package install/remove operations will fail.

### "Extensions not available for firmware X.XX"

Extension patch files are specific to each firmware version. If you've updated to a very new firmware version, the community may not have released compatible patches yet. Check the [xovi-qmd-extensions repository](https://github.com/rmitchellscott/xovi-qmd-extensions) for updates.

### "rebuild_hashtable failed"

Try restarting xochitl manually via SSH:

```bash
ssh root@<device-ip> "systemctl restart xochitl"
```

If the problem persists, try running the rebuild manually:

```bash
ssh root@<device-ip> "cd /home/root && ./xovi/rebuild_hashtable"
```

### Extensions deployed but no visible effect

- Ensure xovi is running: `ssh root@<device-ip> "test -f /home/root/xovi/xovi.so && echo ok"`
- Clear the QML cache and restart: `ssh root@<device-ip> "rm -rf ~/.cache/remarkable/xochitl/qmlcache && systemctl restart xochitl"`

## Technical details

### How it works

Extensions are `.qmd` (QML Diff) files — declarative patches that modify specific properties in the reMarkable UI at startup. The xovi framework's `qt-resource-rebuilder` component intercepts Qt's resource loading and applies these patches before the UI renders.

### Device paths

- Extension files: `/home/root/xovi/exthome/qt-resource-rebuilder/`
- After deploying extensions, the app runs `/home/root/xovi/rebuild_hashtable` and restarts xochitl

### Source and integrity

Extension files are sourced from the [xovi-qmd-extensions](https://github.com/rmitchellscott/xovi-qmd-extensions) repository by Mitchell Scott, licensed under MIT. SHA-512 checksums are verified at build time to ensure file integrity.

### Manual deployment (via SSH)

If you prefer to deploy extensions manually:

```bash
# Copy the QMD file to the device
scp unlockMethodsContent.qmd root@<device-ip>:/home/root/xovi/exthome/qt-resource-rebuilder/

# Rebuild the hashtable and restart
ssh root@<device-ip> "cd /home/root && ./xovi/rebuild_hashtable && systemctl restart xochitl"
```
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"generate-sample-icons": "tsx scripts/generate-sample-icons.ts"
"generate-sample-icons": "tsx scripts/generate-sample-icons.ts",
"validate-xovi": "tsx scripts/validate-xovi-checksums.ts",
"generate-xovi-manifest": "tsx scripts/generate-xovi-manifest.ts"
},
"dependencies": {
"@fastify/cors": "^11.2.0",
Expand Down
91 changes: 91 additions & 0 deletions scripts/generate-xovi-manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Generate manifest.json for bundled xovi extension QMD files.
* Walks server/data/xovi-extensions/<version>/ directories, computes SHA-512
* checksums, and writes the manifest with extension metadata.
*
* Usage: npx tsx scripts/generate-xovi-manifest.ts
*/

import { createHash } from 'node:crypto'
import { readFileSync, readdirSync, writeFileSync, statSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const XOVI_DATA_DIR = resolve(__dirname, '../server/data/xovi-extensions')

const EXTENSION_DEFS = {
unlockMethodsContent: {
filename: 'unlockMethodsContent.qmd',
displayName: 'Unlock Methods Content',
description: 'Bypasses subscription check for using on-device Methods templates and documents.',
tier: 1,
category: 'essential',
},
createPagesRM2Size: {
filename: 'createPagesRM2Size.qmd',
displayName: 'Create Pages (RM2 Size)',
description: 'Forces new pages to use reMarkable 2 dimensions (1404\u00d71872) for cross-device consistency.',
tier: 1,
category: 'page-size',
exclusiveGroup: 'pageSize',
},
createPagesPaperProSize: {
filename: 'createPagesPaperProSize.qmd',
displayName: 'Create Pages (Paper Pro Size)',
description: 'Forces new pages to use Paper Pro dimensions (1620\u00d72160) for cross-device consistency.',
tier: 1,
category: 'page-size',
exclusiveGroup: 'pageSize',
},
preventNotebookZoomOut: {
filename: 'preventNotebookZoomOut.qmd',
displayName: 'Prevent Notebook Zoom Out',
description: 'Forces notebook pages to start at 1x zoom. Designed for Paper Pro Move.',
tier: 1,
category: 'essential',
},
quicksheetUseTemplate: {
filename: 'quicksheetUseTemplate.qmd',
displayName: 'Quicksheet Use Template',
description: 'New quicksheet pages use the same template as the previous page in the notebook.',
tier: 2,
category: 'recommended',
},
}

function sha512(filePath: string): string {
const content = readFileSync(filePath)
return createHash('sha512').update(content).digest('hex')
}

// Discover version directories
const versions = readdirSync(XOVI_DATA_DIR)
.filter(d => /^\d+\.\d+$/.test(d) && statSync(resolve(XOVI_DATA_DIR, d)).isDirectory())
.sort((a, b) => {
const [aMaj, aMin] = a.split('.').map(Number)
const [bMaj, bMin] = b.split('.').map(Number)
return aMaj - bMaj || aMin - bMin
})

// Compute checksums
const checksums: Record<string, string> = {}
for (const version of versions) {
const versionDir = resolve(XOVI_DATA_DIR, version)
const files = readdirSync(versionDir).filter(f => f.endsWith('.qmd'))
for (const file of files) {
const key = `${version}/${file}`
checksums[key] = sha512(resolve(versionDir, file))
}
}

const manifest = {
extensions: EXTENSION_DEFS,
checksums,
supportedVersions: versions,
}

const outPath = resolve(XOVI_DATA_DIR, 'manifest.json')
writeFileSync(outPath, JSON.stringify(manifest, null, 2) + '\n')
console.log(`Wrote ${outPath}`)
console.log(` ${versions.length} versions, ${Object.keys(checksums).length} files`)
53 changes: 53 additions & 0 deletions scripts/validate-xovi-checksums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Validate SHA-512 checksums for bundled xovi extension QMD files.
* Exits with code 1 if any checksum mismatches.
*
* Usage: npx tsx scripts/validate-xovi-checksums.ts
*/

import { createHash } from 'node:crypto'
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const XOVI_DATA_DIR = resolve(__dirname, '../server/data/xovi-extensions')
const manifestPath = resolve(XOVI_DATA_DIR, 'manifest.json')

if (!existsSync(manifestPath)) {
console.error('manifest.json not found — run generate-xovi-manifest.ts first')
process.exit(1)
}

const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as {
checksums: Record<string, string>
}

let ok = true
let checked = 0

for (const [relPath, expectedHash] of Object.entries(manifest.checksums)) {
const filePath = resolve(XOVI_DATA_DIR, relPath)
if (!existsSync(filePath)) {
console.error(`MISSING ${relPath}`)
ok = false
continue
}
const content = readFileSync(filePath)
const actual = createHash('sha512').update(content).digest('hex')
if (actual !== expectedHash) {
console.error(`MISMATCH ${relPath}`)
console.error(` expected: ${expectedHash}`)
console.error(` actual: ${actual}`)
ok = false
} else {
checked++
}
}

if (ok) {
console.log(`All ${checked} QMD file checksums verified.`)
} else {
console.error('\nChecksum validation failed.')
process.exit(1)
}
Loading
Loading