What is UselessOS?
UselessOS is a little sandboxed scripting environment that runs
.useless files. Each script is plain JavaScript, exports
a single main(argc, argv) entry point, and runs inside a
restricted sandbox with a handful of runtime built-ins injected for
you — no arbitrary imports of outer JS modules, no
access to the host app, no DOM access except through the built-ins
documented below. .useless files do have their own
lightweight import/export system for pulling
in other .useless files, described in
Section 12.
This literally has no use. Figure out how to build it yourself, or
just grab the binaries below.
[Site notice] This reference used to be one
giant file. It has been split up so it's easier to find what you need.
Read top to bottom if you're new; jump straight to a section using the
menu on the left if you already know the basics.
Binaries / Downloads
Grab a pre-built binary for your platform below, or build from source.
| Platform |
File |
Size |
|
| Windows |
uselessos-setup-2.2.0.exe |
96.6 MB |
Download |
| macOS |
uselessos-2.2.0.dmg |
126.9 MB |
Download |
| Source |
Repository |
GitHub |
Open Repo |
What changed in this revision
execute() now returns { stdout, stderr, exitCode } instead of a plain string.
- Added
signal(name, handler) and a process object (process.pid, process.argv, process.exit(code), process.kill(pid, signalName)).
- Added
sleep(ms).
- Added environment variables:
getenv, setenv, unsetenv.
- Added a File API:
readFile, writeFile, appendFile, exists, mkdir, remove, listDir.
- Added an
import / export system with a default library manager, libman.
- Split the single reference file into the sections listed in the sidebar.
Nothing here reverts earlier edits — it's the same content, reorganized, plus the additions above.
1–3. Quick Start, Running Scripts, and main()
1. Quick start
Method 1 — Manual write via bash:
echo 'function main(argc, argv) {
printf("Hello, %s!\n", argv[0]);
}' > hello.useless
Method 2 — Write in a code editor, download by URL (recommended):
write your .useless file in VS Code or your editor of
choice, upload it somewhere (or start a local server), then:
download <URL>
You'll now have the file in your current working directory.
chmod +x hello.useless
./hello.useless
running 'hello.useless'
Hello, hello.useless!
Every .useless file must define:
function main(argc, argv) { ... }
...or, if it needs to await anything
(scanf, sleep, execute, timers,
promises, the File API):
async function main(argc, argv) { ... }
main is your entry point — nothing runs before or
after it except the runtime's own setup/teardown (including resolving
any imports at the top of the file — see
Section 12).
2. Running a script
| Method | Example | Notes |
| Direct path | ./hello.useless | Requires execute permission (see chmod) |
| Absolute path | /home/user/hello.useless | Same permission rules |
| PATH alias | hello | Register with addition towards PATH (Sec. 6) |
| Boot service | onbootservices add hello /home/user/hello.useless | Runs automatically at boot |
| From inside another script | await execute("./hello.useless") | See execute() (Sec. 4.3) |
A file must:
- have the
.useless extension
- be marked executable (
chmod +x file.useless)
Otherwise you'll get permission denied or
cannot execute binary file.
3. main(argc, argv)
| Parameter | Type | Description |
argc | number | Argument count, including the program name (argv[0]) |
argv | string[] | argv[0] is the name/path used to invoke the script; argv[1..] are the arguments passed on the command line |
./greet.useless world
function main(argc, argv) {
// argc === 2
// argv === ["./greet.useless", "world"]
printf("Hi, %s! (%d args)\n", argv[1], argc);
}
argv is also reachable anywhere in the script as
process.argv, without threading it through function calls
— see Sec. 9.2.
Declaring main correctly
| You need to... | Declare main as |
| Just print output, no waiting | function main(argc, argv) { ... } |
await scanf(...), await sleep(...), or any File API call | async function main(argc, argv) { ... } |
| Keep running after main logically "returns" (e.g. a setTimeout that later calls bufferscreenexitprogramservices()) | async function main(argc, argv) { ... } recommended, though a plain function also works since the runtime tracks the open screen session for you |
Rule of thumb: if your script uses await
anywhere, main must be async. A non-async
main cannot await, so
const x = scanf(...) without await gives you
a pending Promise object, not the typed value — and
printf("%s", x) will print [object Promise]
instead of the input. The same mistake applies to sleep(...),
execute(...), and every File API call.
4. Runtime Built-ins
These functions are injected into every .useless file's
scope. You do not declare or import them — they're just available
inside main (and any function it calls).
4.1 printf(format, ...args)
Prints formatted text to the terminal (or, if a screen buffer is
open, still writes to the terminal underneath — see
Section 5).
| Specifier | Meaning |
%s | String |
%d, %i | Integer (truncated toward zero) |
%f | Number, printed as-is |
%% | Literal % |
\n in the format string produces a new terminal line.
function main(argc, argv) {
printf("Name: %s\n", "root");
printf("Uptime: %d seconds\n", 42);
printf("Load: %f\n", 0.73);
printf("100%% done\n");
}
4.2 scanf(promptLabel) — async, must be awaited
Prompts for a line of input and resolves with what the user typed
once they press Enter.
async function main(argc, argv) {
const name = await scanf("What's your name? ");
printf("Hello, %s!\n", name);
}
promptLabel is optional; pass "" or omit for a silent prompt.
- Works both in the normal terminal and inside a
bufferscreenclear() overlay session — a floating
input bar appears automatically.
- Only one
scanf() call resolves at a time; always
await before calling it again.
- A pending
scanf() is interrupted immediately if a
SIGINT handler fires (Sec. 9.1).
Common mistake:
// WRONG -- main is not async, so await is illegal here, and
// without await this returns a Promise object, not a string.
function main(argc, argv) {
const name = scanf("Name: ");
printf("Hi %s", name); // prints "Hi [object Promise]"
}
4.3 execute(commandString) — async, must be awaited
Runs a full shell command line (any built-in command, or another
.useless script) as if typed at the prompt, but silently
— nothing is echoed. It talks directly to the one running bash
instance started at BootProcess (it does not spawn a new
instance).
async function main(argc, argv) {
const result = await execute("ls -a /home/user");
printf("%s\n", result.stdout);
if (result.exitCode !== 0) {
printf("command failed: %s\n", result.stderr);
}
await execute("mkdir -p /home/user/backups");
await execute("./other-script.useless some-arg");
}
| Field | Type | Description |
result.stdout | string | Standard output produced by the command |
result.stderr | string | Standard error output produced by the command |
result.exitCode | number | 0 on success, non-zero on failure |
Migration note: earlier runtime versions resolved
execute() directly to the plain-text output string.
printf("%s", await execute("ls")) now prints
[object Object] — use
(await execute("ls")).stdout instead.
Use this to compose scripts out of shell commands or chain scripts
together — or use the File API directly
when you just need to read/write files.
4.4 bufferscreenclear()
Opens a full-viewport overlay (<div id="screen">)
on top of the terminal and returns that DOM element. Use it for
anything more visual than line-by-line printf output.
function main(argc, argv) {
const screen = bufferscreenclear();
screen.innerHTML = '<h1 style="color: white;">Hello, World!</h1>';
setTimeout(() => {
bufferscreenexitprogramservices();
}, 5000);
}
- Calling it again while open reuses the same element (
innerHTML cleared first).
- While open, normal terminal input is hidden. A
scanf() call shows a floating input bar over the overlay (Sec. 5).
- The program is not finished just because main() returns
while a screen session is open — the runtime keeps it running
until
bufferscreenexitprogramservices() is called, or
until SIGINT closes it.
4.5 bufferscreenexitprogramservices()
Closes the overlay opened by bufferscreenclear() and
returns control to the terminal.
- If called synchronously within main's own execution,
it immediately stops the rest of the script — nothing after it runs.
- If called later (setTimeout, event listener,
resolved promise, SIGINT handler, after main has returned), it simply
closes the overlay and lets the program finish cleanly.
function main(argc, argv) {
const screen = bufferscreenclear();
screen.innerHTML = "<p style='color:#0f0'>Press any key to continue...</p>";
const onKey = () => {
document.removeEventListener("keydown", onKey);
bufferscreenexitprogramservices();
};
document.addEventListener("keydown", onKey);
// Ctrl+C also closes this cleanly, even mid-wait:
signal("SIGINT", () => {
printf("Interrupted!\n");
bufferscreenexitprogramservices();
});
}
You do not need to manually remove the #screen element
or reset input state — cleanup is handled automatically, including
on crashes and SIGINT (Sec. 13).
5. Interactive Full-screen Programs
Combine bufferscreenclear(), DOM manipulation, and
scanf() to build simple full-screen interactive tools
(menus, forms, games):
async function main(argc, argv) {
const screen = bufferscreenclear();
screen.innerHTML = "<h2 style='color:#7ee787'>Setup Wizard</h2>";
const name = await scanf("Enter your name: ");
screen.innerHTML += `<p style="color:white">Hi, ${name}!</p>`;
const confirmed = await scanf("Type 'yes' to continue: ");
if (confirmed.toLowerCase() !== "yes") {
screen.innerHTML += "<p style='color:#ff6b6b'>Cancelled.</p>";
await sleep(1500);
bufferscreenexitprogramservices();
return;
}
screen.innerHTML += "<p style='color:#7ee787'>All set!</p>";
setTimeout(() => bufferscreenexitprogramservices(), 2000);
}
scanf() prompts show in the floating input bar over
the overlay, not in screen.innerHTML — print your
own prompt text into the overlay if you want it visible there too.
- Because
main is async and uses
await scanf(...) / await sleep(...), it must
be declared async function main(...).
- Ctrl+C with no
signal("SIGINT", ...) handler
registered: the runtime default closes the overlay and terminates the
script (Sec. 9.1). Register your own handler for a chance to clean up
first.
6–8. PATH Aliases, Background Processes, Boot Services
6. PATH aliases
Register a short alias for a script so it can be run by name instead
of by path:
addition towards PATH "hello.useless" as "hello"
- The path is resolved relative to your current directory at
registration time (or absolute, if given as one).
- Once registered, run it as
hello arg1 arg2 from anywhere.
- Aliases are stored persistently and survive reboot.
- The target file does not need to exist at run time in the same
location it was registered from — it's resolved to the path
stored at registration.
7. Background processes
Any .useless file can run in the background instead of
blocking the terminal. Two ways to mark a script as one:
1. Header comment, first line of the file:
// DEFINE <BACKPROCESS>
function main(argc, argv) {
// long-running work
}
2. Boot service registration — services
started via onbootservices start <name> / at boot run
in the foreground by default unless the script declares
<BACKPROCESS>.
Background processes:
- Print
starting background process 'name' [pid <id>]
when launched. This <id> is the same value the
script sees as process.pid.
- Are listed with
ps.
- Can be reattached to the foreground with
pickup -pid <id> (or pickup <id>),
which awaits completion and surfaces errors (or the exit code passed
to process.exit(code)).
- Can be sent signals via
process.kill(pid, signalName),
or from the shell (kill -SIGINT <id>, if the script
registered a handler).
- Get their own independent execution context — a crash in
one does not affect the shell or other processes.
8. Boot services
Scripts can be registered to run automatically every time UselessOS boots:
onbootservices add mydaemon /home/user/mydaemon.useless
onbootservices list
onbootservices start mydaemon # run it right now, enable it for next boot
onbootservices restart mydaemon # run it again right now
onbootservices pause mydaemon # skip it at the next boot, keep registration
onbootservices remove mydaemon # unregister entirely
onbootservices reset # clear all registered services
At boot, enabled and unpaused services run in registration order,
each printing Starting boot service '<name>'...
before it runs. A foreground boot service that never resolves (e.g. an
interactive script awaiting scanf) will block the rest of
boot — prefer <BACKPROCESS> scripts, or scripts
that finish on their own, for unattended boot services.
9. Signals and the process API
9.1 signal(name, handler)
Registers a handler function that runs when the named signal is
delivered to this script.
| Signal | Delivered when |
"SIGINT" | The user presses Ctrl+C while this script has focus, or another script sends it via process.kill(pid, "SIGINT") |
function main(argc, argv) {
const screen = bufferscreenclear();
screen.innerHTML = "<p style='color:#7ee787'>Running... press Ctrl+C to stop</p>";
signal("SIGINT", () => {
printf("Interrupted!\n");
bufferscreenexitprogramservices();
});
}
- Registering a handler for a signal replaces any previously
registered handler for that same signal in this script.
- If a script never calls
signal(...), SIGINT falls
back to the runtime default: close any open screen buffer and
terminate the script immediately.
- Handlers run even while the script is awaiting something
(
scanf, sleep, a pending execute,
a File API call) — the runtime interrupts the wait to invoke
the handler.
- A handler is expected to actually stop the program. One that
doesn't leaves the process visibly "hung" in
ps even
after Ctrl+C.
9.2 process
An object available in every script's scope:
| Property / method | Description |
process.pid | This script's process id — the same id shown by ps and used with pickup -pid/process.kill. |
process.argv | Same array passed as argv to main — handy for helper functions that don't receive argv directly. |
process.exit(code = 0) | Terminates the script immediately, closing any open screen buffer automatically. code is shown by ps/pickup; non-zero is reported like an uncaught error but without the "Segmentation fault" banner. |
process.kill(pid, signalName = "SIGINT") | Delivers a signal to another running (typically background) process by pid. Only has an effect if the target registered a handler for that signal — otherwise it falls back to its own default handling. |
async function main(argc, argv) {
printf("My pid is %d\n", process.pid);
await sleep(5000);
process.exit(0);
}
// stop a background process by pid, politely
async function main(argc, argv) {
const targetPid = Number(argv[1]);
process.kill(targetPid, "SIGINT");
}
9.3 sleep(ms) — async, must be awaited
Pauses the script for ms milliseconds without blocking
the rest of the shell.
async function main(argc, argv) {
printf("Waiting...\n");
await sleep(2000);
printf("Done.\n");
}
- An in-flight
sleep is interrupted immediately if a
SIGINT handler is registered and fires — the handler runs right
away instead of waiting for the timer.
- Forgetting
await starts the timer but doesn't pause
execution — same "pending Promise" mistake as scanf.
10. Environment Variables
getenv(name) // -> string | null
setenv(name, value)
unsetenv(name)
getenv(name)
Returns the current value of an environment variable as a string,
or null if it isn't set. Two are pre-populated by the
runtime at boot:
function main(argc, argv) {
printf("Home: %s\n", getenv("HOME")); // e.g. "/home/user"
printf("User: %s\n", getenv("USER")); // e.g. "user"
}
setenv(name, value)
Sets (or overwrites) an environment variable.
function main(argc, argv) {
setenv("EDITOR", "nano");
printf("Editor: %s\n", getenv("EDITOR")); // "nano"
}
unsetenv(name)
Removes a variable entirely — a subsequent getenv
call for that name returns null, not an empty string.
function main(argc, argv) {
setenv("EDITOR", "nano");
unsetenv("EDITOR");
printf("%s\n", getenv("EDITOR") === null ? "unset" : getenv("EDITOR"));
// -> "unset"
}
Scope and persistence
Environment variables are shared across the whole boot session, not
private per script — since execute() and every
script talk to the one running bash instance started at
BootProcess, a variable set by one script is immediately
visible to the next script that runs, including background processes
started afterward. They are not persisted like PATH
aliases: a fresh boot starts with only HOME and
USER set, and anything else you setenv is gone.
If you need a setting to survive reboot, write it to a file with the
File API and read it back at startup, instead of relying on
setenv.
11. File API
All File API calls are async and must be awaited.
They operate on the same filesystem that shell commands like
ls, cat, and mkdir use via
execute() — use whichever fits better: the File API
skips the overhead and text-parsing of shelling out for simple
reads/writes, while execute() is still the way to run
other scripts or shell built-ins with no direct function equivalent.
await readFile(path)
await writeFile(path, data)
await appendFile(path, data)
await exists(path)
await mkdir(path)
await remove(path)
await listDir(path)
| Call | Resolves to | Notes |
readFile(path) | file contents as a string | Rejects if path doesn't exist or is a directory — wrap in try/catch if the file may be missing. |
writeFile(path, data) | true on success | Creates the file if missing, overwrites if it exists. Parent directory must already exist. |
appendFile(path, data) | true on success | Creates the file if missing; otherwise appends. |
exists(path) | boolean | Never rejects — true for a file or a directory. |
mkdir(path) | true on success | Creates intermediate directories as needed (like mkdir -p). Resolves true, no error, if it already exists. |
remove(path) | true on success | Deletes a file, or a directory and everything inside it. Resolves true if path doesn't exist. |
listDir(path) | Array<{ name, isDirectory }> | One entry per item directly inside path (not recursive). Rejects if path doesn't exist or isn't a directory. |
async function main(argc, argv) {
if (!(await exists("/tmp/notes"))) {
await mkdir("/tmp/notes");
}
await writeFile("/tmp/notes/a.txt", "first line\n");
await appendFile("/tmp/notes/a.txt", "second line\n");
const contents = await readFile("/tmp/notes/a.txt");
printf("%s", contents);
const entries = await listDir("/tmp/notes");
for (const entry of entries) {
printf("%s%s\n", entry.name, entry.isDirectory ? "/" : "");
}
await remove("/tmp/notes/a.txt");
}
try/catch around calls that can reject
(readFile, listDir) keeps a missing file from
crashing the whole script with a "Segmentation fault" (Sec. 13):
async function main(argc, argv) {
try {
const data = await readFile("/home/test.txt");
printf("%s\n", data);
} catch {
printf("no such file\n");
}
}
12. import / export and libman
.useless files can share code with each other through a
small, deliberately simple import system — separate
from (and not to be confused with) real JS import/
require, which is still not available (Sec. 14).
import statements are the very first thing in a file,
before any other top-level code, main() included. The
runtime resolves all of them up front, before main runs.
import { add } from "math"; // library-name import
import math from "math"; // same library, default form
import { add } from "./mylib.useless"; // static path import
There are two kinds of import source:
- Library name (e.g.
"math", no
/ or leading .) — resolved through
libman, the default library manager, to an actual
.useless file.
- Static path (
./...,
../..., or an absolute /... path) — a
direct reference to another .useless file on disk,
resolved the same way execute("./other.useless")
resolves paths.
The two behave differently on purpose — see below.
12.1 Exporting from a .useless file
// /useless/lib/math.useless
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }
- A file only meant to be imported ("a library file") doesn't need
a
main(). If it has one, it can still be run directly
and imported elsewhere.
export only works at the top level of the file,
alongside import — not conditionally, and not
from inside main.
12.2 Library-name imports vs. static path imports
Library-name imports (resolved via
libman) always bind the whole exported namespace to a
local identifier matching the library's name — regardless of
whether you wrote import { add } from "math" or
import math from "math". Either way, you call
math.add(...):
import { add } from "math";
function main(argc, argv) {
printf("%d\n", math.add(1, 2)); // note: math.add, not add
}
This is deliberate: it keeps every lib import consistent no matter
how many functions you use from it, and avoids collisions when two
libraries happen to export a function with the same name. Think of the
{ add } form here as documentation of intent, not a
destructuring guarantee.
Static path imports, by contrast, support real
destructuring — a named import gives you a bare identifier, a
default import gives you the whole namespace:
import { add } from "./mylib.useless";
add(2, 3); // "add" is directly in scope, no namespace wrapper
import helpers from "./mylib.useless";
helpers.add(2, 3); // default-style import still gives the whole namespace
| Import source | import { add } from X gives you | import name from X gives you |
| Library name ("math") | math.add (namespace, named by the lib) | math.add (same) |
| Static path ("./...") | bare add | name.add (namespace, named by you) |
12.3 libman, the default library manager
libman maps a short library name to the
.useless file that actually implements it — the same
way PATH aliases map a command name to a script, but for importable
libraries instead of runnable commands.
libman list
libman add <name> <path>
libman remove <name>
libman path <name>
Shipped by default:
| Name | Resolves to |
math | /useless/lib/math.useless |
libman add registers a new name, resolved the same
way a static path would be at registration time (relative or
absolute).
libman remove unregisters a name; existing scripts
that import it afterward fail to resolve at load time, before
main runs.
- Registrations persist across reboot, like PATH aliases.
- A library file registered with
libman can itself
import other libraries, whether by library name or by static path
— libraries can be built out of other libraries. A chain of
imports that eventually imports itself is a circular import and is
rejected at load time (before main runs), rather than
causing a runtime crash mid-script.
// /useless/lib/geometry.useless -- a library built on another library
import { add } from "math";
export function perimeter(sides) {
return sides.reduce((total, side) => math.add(total, side), 0);
}
13–14. Error Handling and Limitations
13. Error handling
If your script throws an uncaught error (a real JS error — not
the internal overlay-exit signal, and not a SIGINT handled by
signal(...)), the runtime reports it and moves on without
crashing the shell:
Segmentation fault (core dumped) -- hello.useless
<error message>
Common causes:
main is not defined, or isn't a function.
- A runtime error inside
main (e.g. calling a method
on undefined).
- Forgetting
await before an async built-in
(scanf, sleep, execute, any
File API call), then using its result as if already resolved.
- An unresolved
import — a libman
name with no registration, or a static path that doesn't exist
— which fails before main even runs.
- A rejected File API call (missing file for
readFile,
etc.) that wasn't wrapped in try/catch.
process.exit(code) with a non-zero code is
reported similarly (as an exit status visible to ps/
pickup) but without the "Segmentation fault" banner —
it's a deliberate exit, not a crash.
This is isolated per-script — one broken program does not
corrupt the filesystem or affect other running processes.
14. What's not available inside .useless scripts
To keep scripts sandboxed and predictable, the following are
not injected into .useless scope and will
throw ReferenceError if used directly (aside from what's
reachable through bufferscreenclear()'s returned element,
which does give you real DOM access):
fetch / network access outside of
execute("download <url>")
- Real JS
import / require of arbitrary
modules — the .useless import/export/libman system
is a separate, simpler mechanism for pulling in other
.useless files only, not a way to load npm packages or
host-app code.
- Direct access to the host app's React state or
localStorage
Filesystem access, by contrast, is available
directly — either through the File API (readFile,
writeFile, appendFile, exists,
mkdir, remove, listDir), or by
shelling out with execute(...) (ls,
cat, mkdir, download, etc.
— see help in the shell for the full command list).
Use whichever fits the task better.
Full Example
A tiny interactive counter, demonstrating printf,
scanf, a bufferscreenclear() session that
stays open until the user quits, a SIGINT handler, and a
save-to-disk feature using the File API.
// A tiny interactive counter.
async function main(argc, argv) {
let count = 0;
const savePath = "/tmp/counter-save.txt";
if (await exists(savePath)) {
count = Number(await readFile(savePath)) || 0;
}
const screen = bufferscreenclear();
const render = () => {
screen.innerHTML = `
<div style="font-family: monospace; color: #7ee787; padding: 40px;">
<h1>Count: ${count}</h1>
<p>Type "inc", "dec", or "quit". Ctrl+C also saves and quits.</p>
</div>
`;
};
render();
const save = async () => {
await writeFile(savePath, String(count));
};
signal("SIGINT", async () => {
await save();
printf("Saved at %d. Interrupted!\n", count);
bufferscreenexitprogramservices();
});
while (true) {
const cmd = await scanf("> ");
if (cmd === "inc") count++;
else if (cmd === "dec") count--;
else if (cmd === "quit") break;
render();
}
await save();
bufferscreenexitprogramservices();
}
chmod +x counter.useless
./counter.useless
Related sections:
bufferscreenclear/scanf,
full-screen programs,
signal/SIGINT,
the File API.
↑ back to top
|