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

MethodExampleNotes
Direct path./hello.uselessRequires execute permission (see chmod)
Absolute path/home/user/hello.uselessSame permission rules
PATH aliashelloRegister with addition towards PATH (Sec. 6)
Boot serviceonbootservices add hello /home/user/hello.uselessRuns automatically at boot
From inside another scriptawait 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)

ParameterTypeDescription
argcnumberArgument count, including the program name (argv[0])
argvstring[]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 waitingfunction main(argc, argv) { ... }
await scanf(...), await sleep(...), or any File API callasync 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).

SpecifierMeaning
%sString
%d, %iInteger (truncated toward zero)
%fNumber, 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");
}
FieldTypeDescription
result.stdoutstringStandard output produced by the command
result.stderrstringStandard error output produced by the command
result.exitCodenumber0 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.

SignalDelivered 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 / methodDescription
process.pidThis script's process id — the same id shown by ps and used with pickup -pid/process.kill.
process.argvSame 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)
CallResolves toNotes
readFile(path)file contents as a stringRejects if path doesn't exist or is a directory — wrap in try/catch if the file may be missing.
writeFile(path, data)true on successCreates the file if missing, overwrites if it exists. Parent directory must already exist.
appendFile(path, data)true on successCreates the file if missing; otherwise appends.
exists(path)booleanNever rejects — true for a file or a directory.
mkdir(path)true on successCreates intermediate directories as needed (like mkdir -p). Resolves true, no error, if it already exists.
remove(path)true on successDeletes 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:

  1. Library name (e.g. "math", no / or leading .) — resolved through libman, the default library manager, to an actual .useless file.
  2. 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 sourceimport { add } from X gives youimport name from X gives you
Library name ("math")math.add (namespace, named by the lib)math.add (same)
Static path ("./...")bare addname.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:

NameResolves 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