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 state is kept on the global browser object: `window.LOTW`.
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 use this syntax: it is a strange mixture of the 2 pure forms above
function my_func(){
//...
}
*/
There are two ways to hook user-facing code into the system: Windowed applications and Shell commands.
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`
... 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.