A community driven repository of custom applications and games for the Pip-Boy 3000, hosted on pip-boy.com.
Pip-Boy.com | Discord Community | Bethesda Store | The Wand Company | Espruino | RobCo Industries
- Description
- Creating a new Holotape
- Development Workflow
- Images
- Input handling
- Memory and Performance
- Contributing
- License
Pip-Boy 3000 Holotapes by the community, for the community.
Install on: pip-boy.com
Follow the guide below to create your own custom Holotapes for the Pip-Boy 3000!
[ Index ]
-
Create a directory for the app or game under
holotapes/. Every Holotape must use this structure:holotapes/<YourHolotape>/ ├── app.js ├── app.min.js ├── metadata.json ├── README.md ├── ChangeLog └── assets/ -
Write the unminified source in
app.js. The app must be an anonymous function expression that the Pip-Boy OS invokes; do not invoke it yourself with a trailing(). It must return an uppercase alphanumericidand aremovefunction.Example app:
Expand/Collapse
(function () { const W = h.getWidth(), H = h.getHeight(); let leftWheel = 0, rightWheel = 0, lastInput = 'NONE'; function draw() { h.clear(0); h.setColor(3) .setFontMonofonto28() .setFontAlign(0, 0) .drawString('EXAMPLE', W / 2, 50) .setFontMonofonto18() .drawString('LAST: ' + lastInput, W / 2, 100) .setFontMonofonto16() .setFontAlign(-1, -1) .drawString('LEFT WHEEL: ' + leftWheel, 80, 145) .drawString('RIGHT WHEEL: ' + rightWheel, 80, 175) .drawString('PRESS THE LEFT WHEEL TO RESET', 80, H - 40); } function onKnob1(dir, long) { if (dir) { leftWheel += dir; lastInput = dir < 0 ? 'LEFT UP' : 'LEFT DOWN'; Pip.playSound('SCROLL'); } else { leftWheel = 0; lastInput = long ? 'LEFT LONG PRESS' : 'LEFT PRESS'; Pip.playSound('TAB'); } draw(); } function onKnob2(dir) { if (dir) { rightWheel += dir; lastInput = dir < 0 ? 'RIGHT UP' : 'RIGHT DOWN'; Pip.playSound('SCROLL'); } draw(); } Pip.audioStop(); Pip.onExclusive('knob1', onKnob1); Pip.onExclusive('knob2', onKnob2); draw(); return { id: 'EXAMPLE', notDefault: true, fullscreen: true, remove: function () { Pip.removeListener('knob1', onKnob1); Pip.removeListener('knob2', onKnob2); Pip.audioStop(); h.clear(); }, }; });
-
Add
metadata.json. Asset paths and storage URLs are relative to the Holotape directory. The storage directory is the metadataidconverted to uppercase, with underscores replacing hyphens.{ "id": "example", "name": "Example Holotape", "author": "@your-github-username", "version": "1.0.0", "description": "A short, one-sentence description.", "icon": "assets/icon.png", "previews": [], "type": "app", "readme": "README.md", "storage": [{ "name": "HOLO/EXAMPLE/APP.JS", "url": "app.min.js" }], "storageOptional": [] }The metadata
idmust contain only lowercase letters, numbers, and hyphens;versionmust use semantic versioning; andtypemust be exactlyapporgame. Do not editholotapes/registry.jsonmanually.npm run buildgenerates it from eachmetadata.jsonfile. -
Document the description, controls, installation, tested firmware, and credits in the Holotape's
README.md. -
Add at least one
ChangeLogentry in this format:1.0.0 (yyyy-mm-dd) <pull-request-or-change-link> - Initial release -
Generate
app.min.jsfromapp.jsas described in Build and minification. Both files must behave identically.
[ Index ]
Expand/Collapse
-
Open the Pip-Boy 3000 Holotape Creator/Editor.
-
Create a new Holotape and give it a name.
-
Create or edit the Holotape code in the built-in editor.
-
Test your Holotape on the device using the "Save & Test" button.
The editor's "Encode" button can prepare code for testing. Keep the readable source as
app.jsand the encoded/minified output asapp.min.js. -
Download your files and add them to this repository.
You can use one of the two methods below to upload and test your Holotape:
Expand/Collapse
-
Open the Espruino Web IDE or its GitHub-hosted version.
-
Open your file:
-
Enable Watch File.
-
Edit the app in VS Code or the Web IDE's built-in editor.
-
Enable Settings > Minification > Esprima: Mangle.
-
Set Settings > Minification > Pretokenise code before upload to Yes/Always.
-
Upload to the device for testing.
Minify and Encode your Holotape here:
https://www.pip-boy.com/3000/holotapes/create
This will give you a proper app.min.js from app.js.
Run the repository build after adding or changing metadata:
npm install
npm run buildThis rebuilds holotapes/registry.json, rewrites relative file paths for the
registry, and rejects metadata whose type is not app or game. It does not
generate app.min.js or perform all of the submission checks for you.
[ Index ]
Expand/Collapse
Holotape images must be bitmaps with a maximum color depth of 4bpp. Convert source artwork with the Image Converter.
Prefer one h.drawImage() call over many procedural drawing calls for sprites.
Small sprites can be stored inline:
const sprites = { icon: atob('...') };
h.drawImage(sprites.icon, 120, 80);Larger collections can be stored separately and loaded from the SD card. Defer
large asset loads with setTimeout(..., 0) and call E.defrag() first:
let sprites,
assetTimeout = setTimeout(function () {
E.defrag();
sprites = eval(require('fs').readFileSync('HOLO/MYAPP/IMG.JS'));
h.drawImage(sprites.icon, 120, 80);
}, 0);Any asset timeout must be cleared in remove(). Very large backgrounds can be
streamed into h.buffer with E.openFile() and Uint8Array instead of being
held as another complete image in memory.
Metadata icons must be PNG files. Preview files may be PNG, GIF, or MP4, and their paths must be relative to the Holotape directory.
[ Index ]
Expand/Collapse
Use Pip.onExclusive() when an app needs exclusive control input handling:
function onKnob1(dir, long) {
if (dir === 1) {
// Down / clockwise
} else if (dir === -1) {
// Up / counter-clockwise
} else if (long) {
// Long press
} else {
// Normal press
}
}
Pip.onExclusive('knob1', onKnob1);Pip.onExclusive() is the preferred default. Use Pip.on() only when the app
intentionally needs to coexist with another handler. A setWatch() on
ENC1_PRESS is reserved for unusually latency-sensitive press handling. A
direct watch on a button such as BTN_DATA is appropriate only when no
Pip.on() event exists.
Remove every listener and watch when the app exits:
Pip.removeListener('knob1', onKnob1);
clearWatch(buttonWatch);For text entry, firmware 1.1.4 and later provides a built-in on-screen keyboard:
Pip.createKeyboard(initialText, description, callback);The keyboard takes exclusive control of both knobs and the callback receives the
current text when the user selects Enter. It does not close itself: the call
returns an object with a remove() method, and you must call .remove() on it
(typically inside the callback) before drawing your next screen. On older
firmware there is no global keyboard API; see agents.md for a
showTextEntry-style implementation you can copy if you need to support
pre-1.1.4 devices.
[ Index ]
Expand/Collapse
Main rules:
-
The display is always 480×320. If dimensions are needed repeatedly, declare them once as
const W = h.getWidth(), H = h.getHeight(). -
Every variable consumes a scarce Espruino block. Use
constfor constants,letfor mutable state, nevervar, and inline single-use values. -
Always use the global
hgraphics object directly and chain graphics calls. Do not spend a variable block on an alias forh. -
Use
h.clearRect(), clipping, cached wrapped text, and dirty flags to redraw only changed regions. -
Timer-driven apps normally rely on the OS auto-flush. Do not call
h.flip()from asetInterval()loop. ForPip.onFramerendering, setPip.lastFlip = getTime()before drawing and callh.flip()after drawing. -
Use
"ram"for performance-critical frame functions and"jit"for small, numeric tight loops. Do not use either without a measured need. -
Use
Math.randInt(n)instead ofMath.random(), and use typed arrays for dense numeric data. -
Espruino does not support
async/await, ES modules, template literals,fetch(),XMLHttpRequest, orrequestAnimationFrame(). -
Keep the app scoped:
(function () { // App code here return { id: 'APPID', notDefault: true, fullscreen: true, remove: function () { /* clean up app resources */ }, }; });
-
Clean up every resource created by the app in
remove():remove: function() { Pip.removeListener("knob1", onKnob1); clearInterval(intervalId); clearTimeout(timeoutId); clearWatch(watchId); Pip.audioStop(); h.clear(); }
-
Never call
load()orE.reboot()fromremove(), callPip.remove()at the top of the app, use a bareclearWatch(), or delete/reassign OS globals.
Useful memory checks:
process.memory();
print(E.getSizeOf(this, 1).sort((a, b) => a.size - b.size));
print(E.getSizeOf(Pip, 1).sort((a, b) => a.size - b.size));
print(E.getSizeOf(this['\xFF'], 1).sort((a, b) => a.size - b.size));this['\xFF'] shows timers, watches, and internal runtime state.
Pip.CURRENT can hold the current page or app code.
[ Index ]
Expand/Collapse
-
Fork the repository:
-
Clone your fork and enter the repository:
git clone https://github.com/<my-username>/pip-boy-3000-holotapes.git cd pip-boy-3000-holotapes
-
Sync your fork before starting. On your fork's GitHub page, select Sync fork > Update branch. Then update local
mainand merge it into your working branch:git checkout main git pull origin main git checkout -b <my-branch> git merge main git push origin <my-branch>
To merge all the new updates from the original repository's
mainbranch directly into your checked out working branch:git checkout <my-branch> git pull https://github.com/CodyTolene/pip-boy-3000-holotapes.git main git push origin <my-branch>
Replace
<my-branch>with your branch name. If the branch does not exist yet, create it from the updatedmainbranch with:git checkout -b my-holotape
-
Make the change. New Holotapes must include all files described in Creating a new Holotape.
Submissions must include the original, human-readable source code (
app.js). Pull requests containing only minified code (app.min.js) will be rejected. This is an open source project, and readable source is essential so the community can review changes, maintain and update apps over time, fix bugs, and learn from each other's work. -
Run the repository build and test the Holotape on the device:
npm install npm run build
-
Before opening a pull request, verify:
- The original unminified source (
app.js) is included; minified code alone is not accepted. app.jsstarts with(function() {, ends with});, and is not invoked.- The return object contains a literal uppercase alphanumeric
idand aremovefunction. - Every listener, interval, timeout, and watch is removed or cleared; audio is stopped if used.
remove()exits cleanly withoutload()orE.reboot().- No unsupported language/runtime features or OS-global mutations are used.
app.min.jsexists and behaves exactly likeapp.js.- Metadata uses a unique lowercase ID, semantic version, valid type, valid
relative paths, and the matching
HOLO/<APP_ID>/storage prefix. README.mddocuments controls andChangeLogcontains an entry.- Images are converted bitmaps at 4bpp or less.
- The app can be opened, closed, and opened again without leaking resources.
- Memory usage is acceptable before, during, and after the app runs.
- The original unminified source (
-
Stage, commit, and push your changes. For a branch named
<my-branch>:git add -A && git commit -m "My change description" && git push origin <my-branch>
-
Open a pull request from your working branch to this repository's
mainbranch. Describe the change and include screenshots, GIFs, or video previews when the UI changed.
[ Index ]
This repository is licensed under the MIT License.
All code, holotapes, apps, games, scripts, metadata, documentation, and other contributions submitted to this repository must be licensed under the MIT License unless explicitly stated otherwise by the repository maintainer in writing.
By submitting a pull request or contribution, you agree that your contribution is provided under the MIT License. See CONTRIBUTING.md for details.
See the LICENSE file for details.
SPDX-License-Identifier: MIT
[ Index ]

