JSCore
Jscore or jscore.ewwii is an official JavaScript language support plugin for ewwii. It aims to bring the familiarity of JavaScript to the powerful backend of ewwii. This plugin is built for for users who want the maximum power and the speed at the cost of an heavy Just-In-Time JS engine.
One of the major differences between JSCore and other ewwii language support plugins is that it opts for a modern, self-sufficient architecture where all the configuration can be done directly from the comfort of your javascript file without having to rely on external scripts.
Features
- npm support: Wide range of packages from JS ecosystem
- v8 engine backend: Blazingly fast evaluation
- self-sufficient: No need for external scripts
Installation
You can use the following command to install jscore using eiipm.
# if you prefer prebuilt
eiipm add "Ewwii-sh/jscore.ewwii" --prebuilt --ref <version>
# if you prefer building yourself
eiipm add "Ewwii-sh/jscore.ewwii" # Up to date with "main" branch
eiipm add "Ewwii-sh/jscore.ewwii" --ref <version> # Locked to a specific version
Or you can download it manually from Github Releases.
If you are downloading from the GitHub Releases, make sure to create a plugins/ directory in your
ewwii configuration and put the libjscore.so in there.
Configuration
Jscore follows a fundamentally different style of configuration compared to ewwii. Ewwii, with rhai and nbcl,
both capable languages, went for a declarative style of configuration for ease of use. However, jscore embraces
the imperative nature of JavaScript and builds upon it to create a flexible and powerful configuration system.
Basics
The entry file of jscore is ewwii.js. As always, create the entry file (in this case ewwii.js),
and the ewwii.(s)css to start your configuration off.
Once the files are created, you can start by first importing the widgets:
import * as Widgets from "ewwii/widgets";
This contains all the widgets that you will use to build your configuration.
Creating A Widget
Let's create a label, shall we? To create a label, call the Label function under the Widgets namespace like so:
let myLabel = Widgets.Label();
This creates an empty label. To create a label that holds a value, initialize like so:
let myLabel = Widgets.Label("Hello, World!");
Don't worry about widgets taking in parameters too much for now. Only a few really does that, and it mostly is a syntactical sugar.
Assigning Properties
Now that we have a widget, we should be able to tweak the properties of it. There are two ways in which you can do that. There is no right way to set properties. You can use the method you prefer.
1. Through properties function.
let myLabel = Widgets.Label().properties({
truncate: true,
widget_name: "my_label"
});
The properties function takes in an object (i.e json) which provides a list of properties to set.
2. Through the set_property function.
let myLabel = Widgets.Label();
myLabel.set_property("truncate", true);
myLabel.set_property("widget_name", "my_label");
The set_property function takes in a key and a value. The key being the name of the property,
and the value being the value of the property to set.
Regarding earlier
Widgets.Label("Hello, World!").let myLabel = Widgets.Label("Hello, World!");Is syntactical sugar for:
let myLabel = Widgets.Label().properties({ text: "Hello, World!" });
Appending Children
Some widgets like Box takes in children. You can add a child to these types of
container widgets using the add_child function.
let myLabel = Widgets.Label("Hello, World!");
let myBox = Widgets.Box();
myBox.add_child(myLabel); // set label as child
You would use the same add_child property to set the root child of a window.
Creating a Window
This is the final thing you need to learn to render your first widget. We put together everything we learned till now
to render a window that shows "Hello, World!".
let myLabel = Widgets.Label("Hello, World!");
let myWindow = Widgets.Window("window_name");
myWindow.add_child(mylabel);
Now the final step is to register this window to ewwii so that you can open it with a simple command like ewwii open window_name.
Widgets.register(myWindow);
This will register your window to ewwii.
Full Example
Here is an example code that puts everything together to render a window that shows "Hello, World!":
import * as Widgets from "ewwii/widgets";
let myLabel = Widgets.Label("Hello, World!");
let myBox = Widgets.Box();
myBox.add_child(myLabel);
let myWindow = Widgets.Window("my_window");
myWindow.add_child(myBox);
Widgets.register(myWindow);
Fabulous!
Widget Properties
All the widget supported in jscore and its properties. Just for demonstration, here is how to use Revealer widget and its properties:
import * as Widgets from "ewwii/widgets";
let myRevealer = Widgets.Revealer().properties({
transition: "slideright",
reveal: true,
duration: "500ms"
});
let childLabel = Widgets.Label("Hello!");
myRevealer.add_child(childLabel);
All
These properties apply to all widgets, and can be used on every widget!
Properties
class:stringcss class namevalign:stringhow to align this vertically. possible values: "fill", "baseline", "center", "start", "end"halign:stringhow to align this horizontally. possible values: "fill", "baseline", "center", "start", "end"vexpand:boolshould this container expand vertically. Default: falsehexpand:boolshould this widget expand horizontally. Default: falseeval_ignore:boolskip the automatic re-evaluation of this widget.width:intwidth of this elementheight:intheight of this elementactive:boolIf this widget can be interacted withtooltip:stringtooltip text (on hover)visible:boolvisibility of the widgetstyle:stringinline scss style applied to the widgetcss:stringscss code applied to the widgetcan_target:boolmake the widget targettable to pointer events.focusable:boolmake widget focusablewidget_name:stringcustom widget name
ComboBoxText
Properties
items:vecItems displayed in the combo boxtimeout:durationtimeout of the command. Default: "200ms"onchange:stringruns when an item is selected, replacing{}with the item
Expander
Properties
name:stringname of the expanderexpanded:boolsets whether it's expanded
Revealer
Properties
transition:stringanimation name ("slideright", "slideleft", etc.)reveal:boolwhether the child is revealedduration:durationhow long the transition lasts. Default: "500ms"
Checkbox
Properties
checked:boolinitial checked statetimeout:durationcommand timeout. Default: "200ms"onchecked:stringcommand when checkedonunchecked:stringcommand when unchecked
ColorButton
Properties
use_alpha:booluse alpha channelonchange:stringcommand on color selecttimeout:durationDefault: "200ms"
ColorChooser
Properties
use_alpha:booluse alpha channelonchange:stringcommand on color selecttimeout:durationDefault: "200ms"
Scale
Properties
flipped:boolreverse directionmarks:stringdraw marksdraw_value:boolshow valuevalue_pos:stringwhere to show value ("left", "right", etc.)round_digits:intnumber of decimal placesvalue:floatcurrent valuemin:floatminimum valuemax:floatmaximum valuetimeout:durationDefault: "200ms"onchange:stringcommand on change (use{}for value)orientation:stringlayout direction
Progress
Properties
text: text to show over the progressshow_text: whether to show the textflipped:boolreverse directionvalue:floatprogress (0–100)orientation:stringlayout direction
CircularProgress
Properties
value:floatthe progression value, between (0-100)start_at:floatthe percentage that the circle should start atthickness:floatthe thickness of the circleclockwise:boolwether the progress bar spins clockwise or counter clockwisefg_color:stringforeground color of circlebg_color:stringbackground color of circle
Input
Properties
value:stringset current textplaceholder:stringset a placeholder textonchange:stringcommand on change (use{}for value)timeout:durationDefault: "200ms"onaccept:stringcommand on Enter (use{}for value)password:boolobscure input
Button
Properties
label:stringlabel to show on the buttontimeout:durationDefault: "200ms"onclick:stringcommand on activationonmiddleclick:stringcommand on middle clickonrightclick:stringcommand on right click
Image
Properties
path:stringimage file pathcontent_fit:stringhow the image should fit ("fill", "contain", "cover", "scaledown")can_shrink:boolwhether the image can shrink or notimage_width:intimage widthimage_height:intimage heightpreserve_aspect_ratio:boolkeep aspect ratiofill_svg:stringfill color for SVGs
Box
Properties
spacing:intspacing between childrenorientation:stringdirection of childrenspace_evenly:booldistribute children evenly
Overlay
Properties
None
Tooltip
Properties
None listed
Scroll
Properties
hscroll:boolallow horizontal scrollingvscroll:boolallow vertical scrollingpropagate_natural_height:booluse the natural height if true
EventBox
Properties
orientation:stringlayout directiontimeout:durationDefault: "200ms"onscroll:stringcommand on scroll ({}becomes direction)onhover:stringcommand on hoveronhoverlost:stringcommand on hover exitcursor:stringcursor typeondropped:stringcommand on drop ({}is URI)dragvalue:stringURI to drag from this widgetdragtype:stringtype to drag ("file", "text")onclick:stringcommand on clickonmiddleclick:stringcommand on middle clickonrightclick:stringcommand on right clickonkeypress:stringcommand on any key press ({}becomes the id of the key pressed)onkeyrelease:stringcommand on any key release ({}becomes the id of the key released)
Label
Properties
text:stringtext to displaytruncate:booltruncate textlimit_width:intmax characters to showtruncate_left:booltruncate beginningunindent:boolstrip leading spacesunescape:boolparses escape sequencesmarkup:stringPango markupwrap:boolwrap textgravity:stringtext gravityxalign:floathorizontal alignmentyalign:floatvertical alignmentjustify:stringtext justificationwrap_mode:stringwrap mode ("word", "char", etc.)lines:intmax lines (−1 = unlimited)
Calendar
Properties
day:floatselected daymonth:floatselected monthyear:floatselected yearshow_heading:boolshow headingshow_day_names:boolshow day namesshow_week_numbers:boolshow week numbersonclick:stringcommand with{0},{1},{2}for day/month/yeartimeout:durationDefault: "200ms"
Stack
Properties
selected:intchild indextransition:stringanimation nametransition_duration:intduration in millisecond as number
FlowBox
Properties
orientation:stringlayout directionspace_evenly:booldistribute children evenlyonaccept:stringcommand on Enter (use{}for selected widget's name)selection_model:stringselection model
Graph
Properties
value:floatcurrent valuetype:stringgraph type. possible values: "line" (default), "step-line", "fill", "step-fill"time_range:durationthe range of time to show. Default: "10s"min:floatminimum valuemax:floatmaximum valuedynamic:boolwhether the y range should dynamically change based on value. Default: trueanimate:boolenable smooth scrolling. Default: trueflip_x:boolflip the graph horizontally. Default: falseflip_y:boolflip the graph horizontally. Default: falsevertical:boolif set to true, the x and y axes will be exchanged. Default: falsethickness:floatthe thickness of the line (for line charts). Default: 1.0line_style:stringchanges the look of the edges in the graph (for line charts). possible values: "miter" (default), "bevel", "round"
Driving Widgets
This is how you update a widget after rendering in jscore. This is jscore's alternative to Poll and Listen.
Jscore takes advantage of the superpower of the ewwii wc command and makes it the default way to update widgets
through an API that abstracts all the complexity away.
Exposing a Widget
First, let's learn how a widget can qualify to be updated after rendering. It just simply has to have a widget_name
property set.
let myBox = Widgets.Box().properties({
widget_name: "cool-box"
});
That's it! ewwii wc can now find the widget, which this whole feature is based on.
Setup
To drive a widget render render, you simply need to define a function named after_render and export it.
export function after_render(api) {}
This function should take exactly one parameter, that is the API which you will use.
API
The api parameter that is passed to the after_render function only has one method. That is find.
You can use the find method to find a widget with a specific widget_name and update it.
export function after_render(api) {
const widget = api.find("cool-box");
}
Now the widget variable holds all the API's that can directly update the widget with the name cool-box.
The widget variable holds these methods:
set_propertyadd_classremove_classadd_classesremove_classes
Signature of these methods:
set_property(string, string);
add_class(string);
remove_class(string);
// array of strings
add_classes([string]);
remove_classes([string]);
Full Example
Here is an example that uses all the API's we've discussed.
export function after_render(api) {
// suppose we have a widget
// with name 'cool-box'
const widget = api.find("cool-box");
widget.set_property("orientation", "h");
widget.add_class("red");
widget.add_classes("green", "blue", "orange");
widget.remove_class("green");
widget.remove_classes("red", "blue", "orange");
}
Do note that all of these runs all at once. Ideally, you'd use async combined with other functions to update widgets programmically.
Jscore also has a stream namespace under Tools that provides predefined functions to make updating widgets easier.
Tools
Jscore exposes tools to help with you do most of the work from within JavaScript itself. You can imports the tools like so:
import * as Tools from "ewwii/tools";
stream
The stream namespace. This contains functions that can stream data to a closure. Useful alongisde after_render.
Methods:
All the methods are sync.
time
Example:
export function after_render(api) {
const myLabel = api.find("cool-text");
Tools.stream.time((t) => {
console.log(`System ticked at: ${t.iso}`);
myLabel.set_property("text", t.string);
});
}
cmd
The command namespace. This contains all functions related to commands.
Methods:
All methods are async.
run(cmd)run_read(cmd)
Example:
await Tools.cmd.run("notify-send Hi");
const output = await Tools.cmd.run_read("echo Hi");
console.log(output); // "Hi"
fs
The file system namespace. This contains functions that can modify/interact the file system.
Methods:
All methods are async.
read(path)write(path, content)append(path, contnet)remove(path)exists(path)mkdir(path)readdir(path)stat(path)copy(src, dest)move(src, dest)
Example:
const path = "./example.js";
// Read to a variable
const contents_now = await Tools.fs.read(path);
// Replace contents with "Hello, World!"
await Tools.fs.write(path, "Hello, World!");
// Add previous content back below "Hello, World!"
await Tools.fs.append(path, contents_now);
// Now delete the file
await Tools.fs.remove(path);
// Check if it exists
if (Tools.fs.exists(path)) {
// will not reach
// as we removed it
}
// Make a directory
const directory = "mydir";
await Tools.fs.mkdir(directory);
// read all files (will be empty)
const files = await Tools.fs.readdir(directory);
// get info about the dir
const info = await Tools.fs.stat(directory);
console.log(info);
// copy/move a dir/file
await Tools.fs.copy(directory, "newdir");
await Tools.fs.move("newdir", "newdir_moved");
Builtins
Jscore is built on top of deno_core, which is a very minimal JavaScript evaluation engine which does not support
things like timers, web, and more. To combat this, instead of using deno_runtime, which would make the binary size
huge and increase the compile time by a lot, jscore implements most of these by itself instead.
fetch(url, {method, headers, body})
Supports:
okstatustext()json()
This is the simple fetch implementation of jscore. It is built for simple use cases and not for downloading large files as such. Simple actions like calling API's and extracting JSON will work seaminglessly.
Example:
// The second param is optional
const response = await fetch("https://api.github.com/repos/ewwii-sh/ewwii");
const json = await response.json();
console.log(json)
Timers
The timers like setInterval, setTimeout, etc. does not exist in deno_core. As these are simple functions,
jscore has them built-in.
Example:
setTimeout example:
// Run a function after 2 seconds (2000ms)
const timeoutId = setTimeout(() => {
console.log("This prints after 2 seconds!");
}, 2000);
// Cancel the timeout before it can execute
clearTimeout(timeoutId);
setInterval example:
let counter = 0;
// Run a function every 1 second (1000ms)
const intervalId = setInterval(() => {
counter++;
console.log(`Interval tick: ${counter}`);
// Stop the loop after it runs 3 times
if (counter === 3) {
console.log("Stopping the interval loop.");
clearInterval(intervalId);
}
}, 1000);
Npm
Jscore can work seaminglessly with packages from node_modules/ with the help of esbuild.
This feature not exclusive to npm. It works perfectly fine with pnpm or yarn because all of them use the same node_modules/
directory.
Directly trying to use a package similar to how you would in node wouldn't work in jscore.
To use a package in jscore, you need to prefix esbuild: to the name of the package.
This tells jscore to use esbuild to quickly convert the package into a standalone JavaScript
file which it can easily load.
Example:
Here is an example on how to use lodash.
import lodash from 'esbuild:lodash';
const arr = [1, 2, 3, 4, 5];
console.log(lodash.chunk(arr, 2));
console.log(lodash.sum(arr));
Note:
Make sure that esbuild is installed before using this feature.
Technical Details
The packages that are bundled by esbuild are stored in the .cache/jscore directory to avoid bundling every time
ewwii reloads jscore, which happens very frequently. Once in a while, its a good idea to clear the cache directory
and let jscore repopulate it.
Packages that use node exclusive features like node:fs cannot be loaded by jscore. Although this limitation
can be solved either by stubbing or redirecting to a simpler API, we decided keep it this way because
no one will really needs node exclusive packages to build simple widgets.