System & files

ScriptWeaver reaches the operating system through the global sw object: sw.fs (files), sw.sys (processes, paths, environment), and sw.app (application lifecycle). Dialogs live under sw.dialog, and HTTP is the standard global fetch. The system clipboard is app.clipboard.

Files — sw.fs

Synchronous, text-oriented file operations on the real filesystem — or, with a //zipfs: path, on your packaged app's own bundle (see below). Each throws a JavaScript error on failure, so wrap calls that might fail in try / catch.

sw.fs.writeFile('/tmp/notes.txt', 'hello\nworld');
const text = sw.fs.readFile('/tmp/notes.txt');

if (sw.fs.exists('/tmp/notes.txt')) {
  /* … */
}
Call Returns Notes
readFile(path) string Whole-file contents.
writeFile(path, content) Creates or overwrites.
exists(path) boolean
list(dir) string[] Entry names in dir.
stat(path) object { name, isDir, isFile, size, mtime }.
mkdir(path) Creates the path, including parents.
remove(path) Deletes a file or empty directory.

Reading bundled assets. A path that starts with // (e.g. //zipfs:/app/data.json) reads from your packaged app's bundle, not the real disk — the read operations (readFile, exists, list, stat) understand the virtual filesystem, so an app can load its own data files. writeFile, mkdir, and remove target the real disk only. See Packaging apps.

Processes, paths & environment — sw.sys

Run a program. sw.sys.exec returns a Promise resolving to { stdout, stderr, exitCode }; the options object accepts cwd and env:

sw.sys.exec('git', ['status', '--short'], { cwd: project }).then((r) => {
  console.log(r.stdout, r.exitCode);
});

Open something in its default app — a URL, file, or folder:

sw.sys.open('https://scriptweaver.dev');
sw.sys.open(downloadPath);

Standard locations. sw.sys.paths is an object of OS directories:

sw.sys.paths.home; // user home
sw.sys.paths.config; // per-user config directory
sw.sys.paths.cache; // per-user cache directory
sw.sys.paths.temp; // temporary directory
sw.sys.paths.exe; // the Player executable's path

Environment variables, and a beep:

sw.sys.env.get('EDITOR'); // string, or undefined
sw.sys.env.set('MY_FLAG', '1');
sw.sys.env.has('HOME'); // boolean
sw.sys.beep(); // a system bell

Clipboard — app.clipboard

Read and write the system clipboard via app.clipboard (it lives on the application window, not under sw). Copying is a clear followed by an append — Tk appends to the clipboard rather than replacing it:

// Copy
app.clipboard.clear();
app.clipboard.append('text to copy');

// Paste — the current contents, or '' if empty / not text
const text = app.clipboard.get();
Method Description
clear() Empty the clipboard.
append(data, type?) Append data (optionally a Tk selection type, e.g. STRING).
get(type?) Return the contents, or '' if empty / unavailable.

Archives — sw.zip

Create a .zip archive. sw.zip(outputPath, files) takes an object mapping archive paths to their contents — a string for text, or an ArrayBuffer / typed array for binary:

sw.zip('/tmp/export.zip', {
  'readme.txt': 'Exported by my app\n',
  'data/records.json': JSON.stringify(records, null, 2),
  'logo.png': pngBytes, // an ArrayBuffer / Uint8Array
});

This is for writing archives. To read an asset bundled inside a packaged app, hand its //zipfs:/app/… path to sw.fs.readFile (above) — see Packaging apps.

Low-level I/O — the os and std modules

sw.fs and sw.sys cover the common cases. When you need something they don't model — a raw file descriptor, a device node, a fifo — the QuickJS os and std modules are available to app code as ordinary imports:

import * as os from 'os';
import * as std from 'std';

These are supported surface, not an implementation detail: they are registered for every app on every platform, and they will not be removed or narrowed without notice. What they are not is cross-platform — they are a thin layer over POSIX and Win32, so what you can actually do with them varies by host. open, close, read, write, seek, readdir, pipe, setReadHandler and setTimeout are the load-bearing calls; the QuickJS documentation has the full list.

Reach for them when there is no sw.* equivalent. Where there is one — dialogs, the clipboard, subprocesses, HTTP, quitting the app — prefer it: sw.dialog, app.clipboard, sw.sys.exec, fetch and sw.app.quit() work the same way on every platform and already do the thread handling described next.

Never block the JS thread

Your app has one JS thread, and the GUI shares it. A blocking os.read() on a device with nothing to say freezes the window until data arrives — no repaints, no clicks, no way out. Two mechanisms avoid that, and you usually want both:

Reading a device without stalling the UI looks like this:

import * as os from 'os';
import * as std from 'std';

const fd = os.open('/dev/hidraw0', os.O_RDWR | os.O_NONBLOCK);
if (fd < 0) {
  // os.open returns a negative errno, not an exception.
  throw new Error(`/dev/hidraw0: ${std.strerror(-fd)}`);
}

const buf = new Uint8Array(64);
os.setReadHandler(fd, () => {
  const n = os.read(fd, buf.buffer, 0, buf.length);
  if (n > 0) {
    handleReport(buf.subarray(0, n)); // back on the JS thread — widgets are safe to touch
  }
});

// Later, in this order:
// os.setReadHandler(fd, null);
// os.close(fd);

os.read / os.write take an ArrayBuffer (buf.buffer above), not the typed-array view, and return the byte count or a negative errno.

Device permissions on Linux. Nodes like /dev/hidraw* are root-owned and unreadable by a normal user until a udev rule says otherwise, and the Player cannot install one for you. An open that fails with EACCES — rather than ENOENT — almost always means that rule is missing, so it is worth telling your user exactly that instead of reporting a generic failure.

Application — sw.app

sw.app.quit(); // close the app
sw.app.tclVersion; // e.g. "9.0.3"
sw.app.tkVersion;

sw.app.themes; // every theme name, flat (aliases included)
sw.app.themesByType; // { light: [...], dark: [...] } — engine names only
sw.app.systemDark; // OS prefers dark? true | false | null (unknown)
sw.app.reducedMotion; // get/set: skip widget animations (e.g. StatusBar 'busy')

The theme properties drive runtime theme pickers and the "follow the system theme" startup pattern — see Theming.

Platform — app.windowingSystem / app.platform

Query the host platform when you need to branch on it:

app.windowingSystem; // 'x11' | 'win32' | 'aqua'
app.platform; // { os: 'Linux' | 'Darwin' | 'Windows NT', arch: 'x86_64' | 'arm64' | …, family: 'unix' | 'windows' }

For DPI/density scaling of absolute pixel coordinates use app.scale / app.dp(n) instead — see UI scale.

Help — sw.openHelp / F1

Pressing F1 anywhere opens documentation in its own window, as a separate Player process — so it never blocks or disturbs the running app. The help window opens in your app's current theme (app.theme is passed through), so it matches what the user is looking at. You can also open it yourself, for example from a Help menu:

help.addCommand({ label: 'Documentation', accelerator: 'F1', command: sw.openHelp });

Your app can ship its own manual

If your app bundles a help/ folder (or docs/) with an index.md, F1 opens that — your app's own manual — instead of the ScriptWeaver docs. It works whether your app runs from a .zip bundle or loose from disk (the help folder sits beside your entry script). Write the manual in the same Markdown the viewer renders here; relative links and images resolve against the help folder. Apps that ship no help/ or docs/ fall back to the ScriptWeaver documentation, so F1 always shows something.

help/ is preferred over docs/, so you can keep developer docs in docs/ and the end-user manual your app surfaces on F1 in help/.

You can preview an app's bundled help without launching the app:

scriptweaver --help myapp.zip     # render myapp's help/ (or docs/)
scriptweaver --help ./myapp/      # ... or a project directory
scriptweaver --help               # the ScriptWeaver docs (no target)

Taking over F1

To use F1 for something of your own, replace or clear the global binding:

__native_tcl('bind', 'all', '<F1>', ''); // disable the built-in help key

Next