DEVELOPER'S INTRODUCTION

The system is called "Linux on the Web" (LOTW). It is a thick JS client, which runs in modern web browsers, and has full access to modern Web APIs.

The system's volatile state is kept on the global browser object: `window.LOTW`.

It should be assumed that persistent storage works primarily via a shell accessible, POSIX-like file abstraction layer that uses the origin-private file system API (OPFS) as the physical storage backend. The file system's directory structure is stored via indexedDb. This database-centric indirection enables the implementation of symbolic links as well as hard links in the LOTW file system, making it largely compliant with the published POSIX specifications.

There are certain persistent storage use-cases that use the localStorage API, but these are generally of no interest to system users, or even most developers for that matter. Saving the graphical locations of icons as they exist on the LOTW desktop is one example of using the (extremely simple) localStorage API.

All GUI elements in LOTW are constructed by hand-crafted (vanilla) JS code, such as document.createElement and parentNode.appendChild. Only in the case of the development of windowed applications must developers worry about the construction of their own GUI elements. It is up to the developer to ensure that GUI-heavy frameworks (like React, Angular, etc.) are compatible with the underlying LOTW system.

Constants:

PR: System root directory, e.g. "/home/bob/my_system/"
APPS: Path for the application logic of windowed apps: $PR/apps/
COMS: Path for shell command libraries: $PR/coms/
MODS: Path for generic code: $PR/mods/
SYS_CFG: System config file: $PR/sys/config.js
SHELL: POSIX shell implementation: $MODS/lang/shell.js
DESK: Desktop implementation: $PR/sys/desk.js

The above paths represent the backend files that implement the system itself

In its normal operation, the system (applications or commands) may write to file paths that *look like* those above (e.g. ~/Desktop/MyApp.app or ~/.env), but they must be considered frontend files (stored via Web APIs) with no relation to the backend system.

Coding style:

const my_func = (arg1) => { // Pure stateless functions
//...
};

class myClass { // Pure stateful objects
//...
}

// Do not generally use this syntax: it is a strange mixture of the
// 2 pure forms above
function my_func(){ // Confusing!
//...
}

There are two ways to hook user-facing code into the system: Windowed applications and Shell commands.

I. Windowed Applications (Apps)

The code for these are kept in $APPS

Apps are custom classes, which are kept in files with the following form:

*FILE_BEGIN*
(()=>{"use strict";
const APPNAME = "path.to.AppName"; // File location: $APPS/path/to/AppName.js

// Helper code here...

class App {

constructor(Win){
this.Win = Win; // Handle to a system `Window` object
this.Main = Win.Main; // The Window's client area (an HTML div element)

/* See onloadfile below

Other choices in Win.fmts are:
- TYPED_U8: Uint8Array (default)
- BLOB: Blob
- ARR_BUFF: ArrayBuffer
- PARSED_OR_ERR: try to run the file's contents through JSON.parse, or return the caught Error
*/
this.loadFmt = Win.fmts.TEXT; // String

this.makeDOM();

}
makeDOM(){
// Dynamically generate and style the Window's visible area
// All graphical event handlers (mouse and touch) must be implemented here
}
meth1(){
}
meth2(){
}
meth3(){
}
getContext(){

// Dynamically generate the choices for the app's context menu and return
// them to the system to be rendered

/*
In practice, the spaces in the strings below should be *non-breaking spaces*
*/

return [ // Main menu
   "Save...", ()=>{this.saveFile()},   
   "Choice 1", ()=>{this.meth1()},   
   "Choice 2", [ // Submenu   
     "Sub choice 1", ()=>{this.meth2()},     
     "Sub choice 2", ()=>{this.meth3()},   
   ]
];

}
async getValue(){
let val;
// Serialize the application state here...
return val;
}
async saveFile(){

let ext = "myext"; // Placeholder for how the app's file extension is determined
let rv = await this.Win.saveFile(ext);
if (Number.isFinite(rv)) {
// Success
}
}
onescape(){
// When the app is in an escapable mode, return true.
// Otherwise, let the system window manager take control by returning false
}
onfocus(){
// The Window is now focused
}
onblur(){
// The Window is now blurred
}
onkill(){
// The Window object is being deleted. Application cleanup logic goes here
}
onappinit(appargs={}){
/* Called by the system when an app is:
1) in its "New File" (unsaved) state
2) not saveable, i.e. a media player
*/
}
onloadfile(val){
/* Called by the system when a file's icon is activated
In this case, `val` will be a JS string, given our format preference given in
the constructor
*/
}
onkeydown(e, sym){
/*
1) e: the keydown event object. e.preventDefault may be called to prevent
unwanted behavior

2) sym: a string-like version of the event, such as:

a_: unmodified 'a' key
a_S: Shift + 'a'
a_C: Ctrl + 'a'
a_A: Alt + 'a'

Modifier "precedence" is: C < A < S, giving these possible combinations:
a_CA, a_CS, a_AS, a_CAS

When a key represents a function or non-printable character, it is capitalized
as such:

ENTER,
BACK,
DEL,
PGUP,
PGDOWN,
HOME,
END

*/
}

}

LOTW.apps[APPNAME] = App; // Export the app to the system
})();
*FILE_END*

Application authors may want to register a file extension with the system, with these 2 steps:
1) Locate the `EXT_TO_APP_MAP` object in $SYS_CFG. The keys are the (lowercase) extensions and the values are the "fully-qualified app names" (using the same dotted notation format in $APPNAME above). For example:

const EXT_TO_APP_MAP = {
txt: "TextEdit", // .txt -> $APPS/TextEdit.js
html: "util.HTML", // .html -> $APPS/util/HTML.js
myext: "dev.MyApp" // .myext -> $APPS/dev/MyApp.js
}

2) Locate the `APP_NAME_TO_ICON` object in $SYS_CFG. The keys are the "simple" app names (not fully-qualified). The values are the number-only portions of the Unicode values, encoded in hex. For example:

const APP_NAME_TO_ICON = {
TextEdit: "1f4dd", // i.e. "\u{1f4dd}" -> 📝 (Paper and pencil icon)
HTML: "1f310", // 🌐
MyApp: "1f680" // 🚀
}


There are several methods for launching applications in "New File" states (i.e. without a file icon to double-click in the interface)

1) Menu-driven:

In $SYS_CFG, locate `APPLICATIONS_MENU`, which looks like:
const APPLICATIONS_MENU = [ // JS Array
"Text Editor", "TextEdit",
"Your App", "path.to.YourApp"
];

An option for "Your App" will then appear in the `Applications` submenu of the main system menu.

2) Keyboard-driven:

In $DESK, search for the string "SHORTCUTS1" or "SHORTCUTS2", depending on whether you want to give any focused windows the chance to consume the given keydown event. To associate a "sym" with an application, add a case clause to the relevant switch statement, for example:

switch(sym) {
//...
case "m_CAS": return open_app("path.to.MyApp");
//...
}

3) Icon-driven:

Run the following command in the terminal to generate an icon on the desktop (or any other writable path):

`appicon path.to.MyApp > ~/Desktop/MyApp.app`

... then when the icon is activated, the system will attempt to load an application at $APPS/path/to/MyApp.js. Note: while the name of the icon may be arbitrary (MyApp *or* Whatever, etc), the `.app` extension is necessary.

II. Shell Commands


Most developers will implement their shell commands in separate files within $COMS, but they may also be kept as "builtins" in $SHELL.

The files kept in $COMS are "command libraries", and look like:

*FILE_BEGIN*
(()=>{"use strict";
const LIBNAME="path.to.mycomlib"; // File location: $COMS/path/to/mycomlib.js

const globals = LOTW.globals;
const {
ShellMod,
// Other imports here
} = globals;
const {
Com // Base command class to be extended
} = ShellMod.comClasses;

// Helper code here...

/*

Commands return only when the `end` method of the `Com` class is called. The numerical value is the error code that is returned to the shell. `ok` and `no` are convenience methods that take an optional string argument to be printed to the terminal (colored green or red), with `end` called internally (returning E_SUC or E_ERR).

*/
class com_mycom extends Com {
static getOpts(){
//This tells the shell how to parse the user-supplied args
  return {   
     SHORT: {// Short options     
       a: 1, // No arg: '-a'       
       b: 2, // Opt arg: '-b=true' or '-b'       
       c: 3 // Req arg: '-c "hello 123"'     
     },     
     LONG: { // Long options     
       all: 1, // No arg: '--all'       
       bin: 2, // Opt arg: '--bin=true' or '--bin'       
       chars: 3 // Req arg: '--chars="hello 123"'     
     }   
  }
}
async init(){
// Load assets, parse user-supplied args and opts, etc
// The `run` method will only be called by the shell after awaiting this method
const {args, opts} = this;
/*

args: an array of strings

opts: an object that maps `opt => val`, such that `val` is one of:

1) String: user-supplied arg value
2) Boolean: true

Using the above getOpts example:

`mycom --all file.txt hello -b=there world --chars="something smart" -- -z`

...gives:
args: ["file.txt", "hello", "world", "-z"]

opts: {
all: true,
b: "there",
chars: "something smart"
}

Note: without the '--', '-z' would have been interpreted as an option, and would have caused a shell error, since 'z' was not "declared" by the object returned from getOpts.

*/

}
async run(){
// Run the main code.
// This will not be called if the command was terminated by `init`,
// i.e. if `end`, `ok` or `no` was called there
// These get printed to the terminal with helpful colors
this.inf("Info msg..."); // Blue
this.suc("Success msg..."); // Green
this.wrn("Warning msg..."); // Yellow
this.err("Error msg..."); // Red

// Sent into pipes, redirected to files, captured by command subs, or
// printed to the terminal
this.out("Write to stdout");

// *Any* data type may be sent into pipes, but the receiving command should
// be expecting the given type
let myobj = new myClass();
await myobj.doIt(1234);
this.out(myobj); // Are we in a pipe that expects an instance of myClass?

// Return success
// Only the first invocation of `this.end` has any effect
this.ok(); // Calls `this.end(E_SUC)` w/ no message

// prints the helpful message and calls `this.end(E_ERR)`
// this.no("You should have done X Y Z!");

}

}

const coms = {
mycom: com_mycom
// Other commands here
};

const onkill = ()=>{
// Command libraries may need to be "killed" when they are being developed.
// Cleanup logic goes here.
};

LOTW.coms[LIBNAME] = {coms, onkill}; // Export the library to the system.

})();
*FILE_END*

To access the above command (`mycom`) in the system shell, the command library: `path.to.mycomlib` must be imported into the terminal's environment like such:

`import path.to.mycomlib`

Or to auto-import command libraries, create a file in ~/.env with the following line:

`IMPORT_COM_LIBS=fs,path.to.mycomlib`

... and when a terminal is opened, the system will attempt to import the command libraries at: $COMS/fs.js and $COMS/path/to/mycomlib.js