/graviton

WebUI library for Gauche

Primary LanguageScheme

Graviton

Table of contents

Overview

Graviton is a library to provide Web-based UI for a standalone Gauche program. You can make UI with HTML, CSS, and JavaScript and integrate the UI into your program.

Graviton also provides JavaScript FFI with S-expression syntax so that you can embed JavaScript code naturally in your program.

You need a modern web browser to access Graviton's Web-based UI. However, Graviton provides an Electron-based client (graviton-player) to interact with the UI. Your program can act as a native GUI application using graviton-player.

CAVEAT: Graviton is a kind of "Web application framework." However, it is designed for one or a few clients. Graviton has no particular restrictions on the number of clients, but it may not be efficient when many clients use it.

Installation

You need the latest version of Gauche and Gauche-makiki.

$ ./configure
$ make
$ make install

If you want to install graviton-player, the latest node and npm are required.

$ make build-player
$ make install-player

Note for WSL user

If you use WSL, the configure script sets graviton-player architecture to win32-x64. You need to install Wine to build the windows binary on WSL.

If you want to use linux-x64 graviton-player with WSLg, you need to specify "--with-wsl=" (no args to this option) for the configure script (./configure --with-wsl=).

Getting started

This simple program opens the "Hello, world" window. This window is closed if you press any keys.

(use graviton)
(use text.html-lite)

(define (main args)
  (with-window (grv-window :body (html:body "Hello,world"))
      ()
    ;; Waits for a keyup event.
    (jsevent-await window "keyup" ())
    ;; Closes this window if possible.
    (close-window)))

grv-window defines a window UI with HTML. with-window opens the specified window (or just waits for a request from the web browser if you don't install graviton-player) and invokes a code after the window is opened.

Here is an example of calling JavaScript.

(use graviton)
(use text.html-lite)

(define (main args)
  (with-window (grv-window :body (html:body (html:div :id "div-block")))
      ()
    ;; Sets "Hello, world" in the div element.
    (let1 div (document'get-element-by-id "div-block")
      (set! (~ div'inner-text) "Hello, world"))
    ;; Waits for a keyup event.
    (jsevent-await window "keyup" ())
    ;; Closes this window if possible.
    (close-window)))

Graviton provides <jsobject> class, a proxy to a JavaScript object. You can call a method of the object with (jsobject 'method args ...) and can access a property using Gauche's slot accessor (e.g. (slot-ref jsobject 'property)).

window and document in Scheme are pre-defined global objects representing window and document in JavaScript.

NOTE: You may feel weird about the usage of document in the example code. This program can accept multiple requests, so actual JavaScript document objects must be different. In fact, window and document are not <jsobject>. They are <jsobject-provier> which can return <jsobject> in run-time. The returned objects are different in the different connections. You can use <jsobject-provider> on behalf of <jsobject>.

Here is an example embedding JavaScript code.

(use graviton)
(use text.html-lite)

(define (main args)
  (with-window (grv-window :body (html:body (html:div :id "div-block")))
      ()
    ;; Sets "Hello, world" in the div element.
    (jslet ((text "Hello, world"))
      (let1 div (document.getElementById "div-block")
        (set! div.innerText text)))
    ;; Waits for a keyup event.
    (jsevent-await window "keyup" ())
    ;; Closes this window if possible.
    (close-window)))

This code is equivalent to the previous example. The difference is that JavaScript code sets the text "Hello, world". You can embed a JavaScript code with (jslet (form ...) body ...) macro. The body is JavaScript in S-expression. It is translated to an actual JavaScript in compile-time and sent to the client when it accesses this page.

If you want to return values from JavaScript, you can use (jslet/await (form ...) body ...) macro.

(use graviton)
(use text.html-lite)

(define (main args)
  (with-window (grv-window :body (html:body (html:div :id "div-block")))
      ()
    ;; Sets "Hello, world" in the div element and prints the body in HTML.
    (print (jslet/await ((text "Hello, world"))
             (let1 div (document.getElementById "div-block")
               (set! div.innerText text)
               (respond document.body.innerHTML))))
    ;; Waits for a keyup event.
    (jsevent-await window "keyup" ())
    ;; Closes this window if possible.
    (close-window)))

Calling JavaScript is asynchronous, so jslet/await doesn't block a thread. jslet/await creates a continuation, and the continuation is called after the JavaScript code returns values. Until then, the thread can do other things. For example, there is an event handler in this thread. The handler can run in this thread before the values return.

There are several code examples under examples/ of this repository. They might be helpful to understand Graviton.

Module: graviton

Configuration

(grv-config :key host protocol client port access-log error-log iframe-window?)
Configures Graviton.
host
Hostname of this server. The default is "localhost".
protocol
Protocol ("http" or "https") to connect this server. The default is "http".
mode
Operation mode of Graviton.
'browser
The program opens a main window with Web browser automatically, and it exits if the window is closed. graviton uses the system default Web browser. If you set BROWSER environment variable, the browser is used instead of the system default.
'player
The program opens a main window with graviton-player automatically, and it exits if the window is closed.
'server
The program waits for the client requests, and can accept requests from multiple clients.
#f (default)
'player if graviton-player is installed. Otherwise, 'browser.
port
Port number to receive requests. If you specify 0, a free port will be assigned automatically. The default is 0 if the client type is 'player, 8080 if the client type is 'browser.
access-log
error-log
The destination of log. #f (no log), #t (stdout), string (filename) or <log-drain> object. The default is #f.
iframe-window?
Whether iframe is used or not to open a new window in the program. #t uses iframe, so the new window is opened inside the main window. #f uses window.open, so the new window opens independently of the main window. The default is #f if the client type is 'player, #t if the client type is 'browser.
(grv-config-parameter name)
Returns the current value of the setting. name must be one of these symbols, port, host, protocol, client, access-log, error-log or iframe-window?.
(client-is-player)
Returns #t if the current client is graviton-player.
(client-is-browser)
Returns #t if the current client is Web browser.

Window

When Graviton receives a request from the client, it creates a new window instance and assigns a thread (called "worker") to the window instance. The code running in the worker can operate the associated window only.

(grv-window :key title css js head body width height resizable? show?)
Returns a window.
title
The title of this window.
css
CSS used in this window. You can specify the filename or SxCSS. If you want to use multiple css, you can pass them by repeated :css keyword parameters, like (grv-window :css "first.css" :css "second.css").
js
JavaScript filename used in this window. If you want to load multiple JavaScript files, you can pass them by repeated :js keyword parameters.
head
<head> element of this window.
body
The contents in <body> element of this window.
width
The width of this window.
height
The height of this window.
resizable?
Whether this window is resizable or not.
show?
Whether this window is visible or not. The default is #t. This option works only for graviton-player.
(with-window window (elements ...) body ...)
Opens window, then execute body ... in a newly created worker. elements ... are IDs of HTML elements, which will be bound to the same names of local variables before the execution. #f will be bound to the variable if the element is not found.
(grv-title)
Returns the title of the current window.
(grv-title-set! title)
Updates the title of the current widnow.
(close-window)
Closes the current window, and terminates workers related to the window.

Window parameter

Graviton provides <window-parameter> to hold values per window instance. One <window-parameter> object can hold each window instance's value, so you can define it as a global variable and refers to it from each window.

(make-window-parameter value ...)
Creates <window-parameter> object with the initial values value ....
(make-window-parameter* thunk)
Creates <window-parameter> object, but the initialization is delayed until a window instance is created. When a new window instance is created, the <window-parameter> object will be initialized with the return values of thunk.
(window-parameter-atomic-ref window-context window-parameter proc)
Calls proc with the current values of window-parameter in window-context, while locking window-parameter.
(window-parameter-atomic-update! window-context window-parameter proc)
Calls proc with the current values of window-parameter in window-context while locking window-parameter, and updates the values in window-parameter by the returned values from proc.
(window-parameter-ref window-context window-parameter)
Returns the current values in window-parameter.
(window-parameter-set! window-context window-parameter vals...)
Sets vals... to window-parameter in window-context.
(object-apply (window-parameter <window-parameter>) value ...)
Returns the values in window-parameter if no value ... are specified. Otherwise, updates the values in window-parameter with value ....

(setter object-apply) is also defined, so you can use it in generalized set!.

Information of the current request

(query-parameters)
Returns query parameters of the current request.
(user-agent)
Returns User-Agent of the current request.

Routing

(bind-url-path url-path file-path)
(bind-url-path url-path proc)
Binds file-path to url-path, so that the file is accessible with the URL.

You can also bind a procedure proc to url-path. proc is called when the URL is requested, and returns the result of proc as the response. proc must returns a body and the content-type.

(file->url filename :optional content-type)
Allocates an URL for the specified filename. If the URL is already allocated for the file, the same URL returns.

If content-type is omitted, it will be estimated from filename.

(data->url data content-type)
Allocates an URL for the data.
(json->url json)
Allocates an URL for the json. json must be a list or a vector.
(sxml->url sxml)
Allocates an URL for the sxml.

Autoloading CSS

(autoload-css css-path ...)
Specifies CSS paths css-path ... to be loaded in all window instances.

Working with JavaScript

<jsobject> represents a JavaScript object. jslet/await can return the object if the returned value is a non-basic JavaScript object. You can call the object's method and access the object's property with it. You can also pass the object to JavaScript world with jslet again.

Graviton defines several JavaScript classes (see below for the pre-defined class list). You can use "kebab-case" symbol as method and property name for the pre-defined classes. For example, you can call document.getElementById(...) using (document'get-element-by-id ...) and refer document.innerHTML using (slot-ref document 'inner-html).

(object-apply (jsobj <jsobject>) (method <symbol>) arg ...)
Calls method of jsobj. method is a kebab-case symbol.
(object-apply (jsobj <jsobject>) (method <string>) [:result] arg ...)
Calls method of jsobj, but method is the original JavaScript method name. You can use this style for a non-predefined JavaScript class's method call. If you want to get the result of the method, you need to set :result keyword.

(ref (jsobj <jsobject>) (property <symbol>))
((setter ref) (jsobj <jsobject>) (property <symbol>) value)
Same as (slot-ref jsobj property) and (slot-set! jsobj property value). property is a kebab-case symbol.
(ref (jsobj <jsobject>) (property-name <string>))
((setter ref) (jsobj <jsobject>) (property-name <string>) value)
Refers the object property with the original JavaScript property name. You can use them for an object of non-predefined JavaScript classes. For example, you can access foo.barBaz with (ref foo "barBaz").

Graviton defines these JavaScript objects as global.

  • window refers to window in JavaScript.
  • document refers to document in JavaScript.
  • audio-context refers to an instance of AudioContext. This object is automatically created by Graviton. You can use it for WebAudio API.

NOTE: In the current browser implementation, AudioContext is initially in the "suspended" state and must be enabled by user interaction. You don't need to implement it because Graviton takes care of this operation.

Pre-defined JavaScript classes

These classes inherit <jsobject>, and their hierarchy is the same as the corresponding JavaScript class hierarchy.

  • <event-target> for EventTarget
  • <node> for Node
  • <node-list> for NodeList
  • <css-style-declaration> for CSSStyleDeclaration
  • <element> for Element
  • <html-element> for HTMLElement
  • <document> for Document
  • <html-body-element> for HTMLBodyElement
  • <html-image-element> for HTMLImageElement
  • <window> for Window
  • <screen> for Screen
  • <blob> for Blob
  • <html-media-element> for HTMLMediaElement
  • <html-audio-element> for HTMLAudioElement
  • <html-video-element> for HTMLVideoElement
  • <event> for Event
  • <html-canvas-element> for HTMLCanvasElement
  • <canvas-rendering-context> for CanvasRenderingContext
  • <image-data> for ImageData
  • <canvas-gradient> for CanvasGradient
  • <canvas-pattern> for CanvasPattern
  • <text-metrics> for TextMetrics
  • <dom-matrix> for DOMMatrix
  • <base-audio-context> for BaseAudioContext
  • <audio-context> for AudioContext
  • <offline-audio-context> for OfflineAudioContext
  • <audio-node> for AudioNode
  • <analyser-node> for AnalyserNode
  • <audio-buffer> for AudioBuffer
  • <audio-scheduled-source-node> for AudioScheduledSourceNode
  • <audio-buffer-source-node> for AudioBufferSourceNode
  • <audio-destination-node> for AudioDestinationNode
  • <audio-listener> for AudioListener
  • <audio-param> for AudioParam
  • <biquad-filter-node> for BiquadFilterNode
  • <channel-merger-node> for ChannelMergerNode
  • <channel-splitter-node> for ChannelSplitterNode
  • <constant-source-node> for ConstantSourceNode
  • <convolver-node> for ConvolverNode
  • <delay-node> for DelayNode
  • <dynamic-compressor-node> for DynamicCompressorNode
  • <gain-node> for GainNode
  • <iir-filter-node> for IIRFilterNode
  • <media-element-audio-source-node> for MediaElementAudioSourceNode
  • <media-stream-audio-destination-node> for MediaStreamAudioDestinationNode
  • <media-stream-audio-source-node> for MediaStreamAudioSourceNode
  • <offline-audio-completion-event> for OfflineAudioCompletionEvent
  • <oscillator-node> for OscillatorNode
  • <panner-node> for PannerNode
  • <periodic-wave> for PeriodicWave
  • <stereo-panner-node> for StereoPannerNode
  • <wave-shaper-node> for WaveShaperNode

Canvas utilities

Graviton provides these functions for Canvas.

(f32vector->dom-matrix f32vec)
Converts <f32vector> to <dom-matrix>.
(f64vector->dom-matrix f64vec)
Converts <f64vector> to <dom-matrix>.
(dom-matrix-copy dom-matrix)
Copies <dom-matrix> object.
(image-data-update! image-data data)
Updates the data of image-data with data. image-data is a instance of <image-data>, and data must be <u8vector>.

This function is identical to imageData.data.set(data) in JavaScript. Writing this property-method chain is wordy in the current Graviton JavaScript object syntax. It is provided for your convenience.

JavaScript event utilities

You can use (event-target'add-event-listener ...) for JavaScript event handling. A scheme procedure can be passed to the listener, so you can catch the event in Scheme world.

However, the procedure will be called with a JavaScript event object. If you need a value in the event, it needs addtional JavaScript FFI requests and needs to wait for the response. Graviton provides several event utilities to reduce such FFI request/response.

(jsevent-callback-set! (jsobj <jsobject>) event-type prop-specs proc :key (use-capture? #f))
Sets a listener proc to event-type of jsobj. You can specify a list of properties with prop-specs. proc will be called with the values of the properties, so that no additional JavaScript FFI request/response are needed.

proc-specs must be a list of property names. For example, '("code" "key") means event.code and event.key. These values are passed to proc. You can also use a property chain, like '("relatedTarget.id"), an index reference, like '("results[0]"), or their combination.

use-capture? is the same to EventTarget.addEventListener. If you want to capture the event in bubbling phase, sets it to #t.

(jsevent-callback-delete! (jsobj <jsobject>) event-type :key (use-capture? #f))
Deletes the listener of the specified event.
(on-jsevent jsobj event-type (arg ...) body ...)
(on-jsevent jsobj (event-type :use-capture? use-capture?) (arg ...) body ...)
This is a convenient macro to set an event listener. When an event is triggered, the properties of the event are bound to arg ..., then body ... are executed.

The property of the event will be bound to corresponding arg. For example, event.key will be bound to arg if arg is key. You can also use a kebab-case symbol like related-target. event.relatedTarget will be bound in this case. If you want to specify the property name explicitly, use (arg "eventName") style.

(request-animation-frame-callback! proc)
Registers proc as a callback before repaint. proc takes one argument, the current time in millisecond.

This function is equivalent to window.requestAnimationFrame(proc).

(cancel-animation-frame-callback! proc)
Removes proc from repaint callback.
(on-animation-frame (arg) body ...)
(on-animation-frame :priority priority (arg) body ...)
This is a convenient macro to register an animation frame callback.
(jsevent-await jsobj event-type prop-specs :key use-capture?)
Waits for event-type of jsobj, and returns the properties of the event, which are specified by prop-specs.

If prop-specs is '(), returns #<undef>.

Worker

Graviton provides Worker (<grv-worker>) to support an asynchronous mechanism. Worker can run code, and the code can yield its execution to other code. One worker is created when a window is opened. It is called "main worker". You can create a worker yourselves to run code in the background.

Worker also has a messaging mechanism. Each worker can communicate with other workers.

(make-worker thunk :key name size)
Makes new worker. thunk will run when the worker starts.

You can specify these keyword parameters.

name
The name of the worker.
size
The number of threads. The default is 1.
(worker-run worker)
Executes worker.
(grv-worker [:name name] [:size size] body ...)
This is a handy macro to make and run new worker. The keyword parameters are the same as make-worker.
(current-worker)
Returns the current worker in which the current code runs.
(main-worker)
Returns the main worker.
(worker-close worker)
Stops receiving messages of worker. Pending messages will be processed.
(worker-shutdown worker)
Stops receiving messages of worker, and discards pending messages.
(worker-active? worker)
Returns whether worker can receive a message or not. (worker-active? worker) returns #f after (worker-close worker) is called.
(current-priority)
Returns the current priority of the current running code.
(add-message-handler! message proc :key priority)
Registers a message handler proc for message.

The received messages will be processed from higher priority. You can specify priority of this event. priority must be one of 'low, 'default or 'high.

(delete-message-handler! message)
Removes a message handler for message.
(define-message message (arg ...) [:priority priority] body ...)
This is a handy macro to define a message handler, same as add-message-handler!.
(object-apply (worker <grv-worker>) message arg ...)
Sends message to worker, and returns <grv-promise> object which will hold the results of the message handler. You extract the results with (await promise).

Asynchronous utilities

(asleep time)
(asleep sec)
Suspends the current running code until the time time is reached or the number of seconds sec elapses.
(when-time-passed sec body ...)
Executes body ... when the number of seconds sec passed after the last execution.

For example, body ... will run every 0.1 second in this case.


(while #t
  (when-time-passed 0.1
    body ...))

Graviton promise

(await promise)
Waits until promise has values, then returns the values.
(disable-async-wait)
(disable-async-wait bool)
disable-async-wait is a parameter to control whether await can yield the right of execution to other code or not. If this parameter is #t, await blocks the current thread until values are set in <grv-promise>.

The default is #f (can yield, don't block), and you don't need to change the parameter. However, you may change the parameter to #t if someone calls (reset ...). Graviton's asynchronous mechanism is built on a partial continuation in worker, but another partial contitnuation can break the asynchronous mechanism (for example, implicit delimited contiations by Scm_Eval).

Channel

<channel> is a queue which supports Graviton's asynchronous mechanism. You can use it for inter-worker communication.

(make-channel)
Makes a channel.
(channel-send channel value ...)
Adds value ... to channel.
(channel-recv channel :optional fallback)
Retrieves a value from channel. If no values exist in it, waits until a value is added or fallback is returned.

If channel is closed, returns <eof-object>.

(channel-close channel)
Closes channel.
(channel-closed? channel)
Returns whether channel is closed or not.

Concurrency and Parallelism

(concurrent body ...)
Executes body ... in concurrent. It means the body will be executed in the current worker after the current code yields the execution.

This macro returns <grv-promise>, so that you can get the results of body ... from it.

NOTE: body ... may be executed in parallel if the current worker has multiple threads.

(concurrent/await body ...)
It is identical to (await (concurrent body ...)).
(parallel body ...)
Executes body ... in parallel. It means the body is executed in newly created worker.

This macro returns <grv-promise>, so that you can get the results of body ... from it.

(parallel/await body ...)
It is identical to (await (parallel body ...).

REPL

(grv-repl :optional reader evaluator printer prompter repl-group)
Starts read-eval-print loop. You must start this REPL in a worker. If multiple REPLs are started, one of them is active in a REPL group. If repl-group is omitted, the default REPL group, which is globally defined, will be used.
(make-repl-group)
Makes a new REPL group.
(next-repl :optional repl-group)
Activates the next REPL in repl-group. If repl-group is omitted, the current REPL's group will be used.
(list-repl :optional repl-group)
Returns a list of REPL in repl-group. If repl-group is omitted, the current REPL's group will be used.
(select-repl repl)
Activates the specified REPL. The repl must be in a group of the current REPL.

Module: graviton.grut

One of the motivations to develop Graviton is to quickly make an old-style computer UI (the days of CUI, i.e., text console + single graphics screen). graviton.grut, which stands for "GRaviton Utility Toolkit," provides miscellaneous utilities to support such UI.

(load-image url :key on-error)
Loads an image from url and returns <html-image-element> object (which is HTMLImageElement in JavaScript).

The keyword argument :on-error can be a keyword :error (default) or #f. If it's the former, an error is signaled when the image can't be loaded from the URL. If it's the latter, load-image just returns #f.

(load-audio url :key on-error)
Loads an audio data from url and returns <html-audio-element> object (which is HTMLAudioElement in JavaScript).

The keyword argument :on-error can be a keyword :error (default) or #f. If it's the former, an error is signaled when the audio data can't be loaded from the URL. If it's the latter, load-audio just returns #f.

(alist->style alist)
Returns a string which is for the style attribute of HTML element. alist is an alist, the key is an style attribute name and the value is the attribute's value. If the value is #f, the style attribute will be ignored.
(grut-canvas-window width height :key id title background-color window-width window-height resizable? fit margin
Creates <grv-window> which has a <canvas> element. width and height are the resolution of the canvas.
id
The element ID of this <canvas> element. The default is "canvas".
You can specify a list of IDs to make multiple layered canvases. The first ID points to the backmost canvas, and the last ID points to the foreground canvas.
title
The title of this window.
background-color
The background color of this window.
window-width
The width of this window.
window-height
The height of this window.
resizable?
Whether this window is resizable or not. The default is #t.
fit
How to fit the size and position of this <canvas> element to this window. The value must be one of these values.
'contain (default)
The canvas is expanded or shrank to fit the window with keeping the aspect ratio. If the canvas's aspect ratio and the window's aspect ratio are different, the canvas will be "letterboxed".
'cover
The canvas is expanded or shrank to fit the window with keeping the aspect ratio. If the canvas's aspect ratio and the window's aspect ratio are different, the canvas will be clipped to fit.
'fill
The canvas is expanded or shrank to fit the window without keeping the canvas's orignal aspect ratio. The entire canvas will completely fill the window.
'none
The canvas will not be resized.
margin
The margin of this <canvas> element.
(grut-text-window :key id title column row font font-size color background-color window-width window-height resizable? fit scrollbar? padding
Creates <grv-window> which has a <grut-text> element.
id
The element ID of this <grut-text> element. The default is "text-console".
You can specify a list of IDs to make multiple layered text elements. The first ID points to the backmost text, and the last ID points to the foreground text.
title
The title of this window.
column
The number of columns of this <grut-text> element. The width of <grut-text> will be computed with this value and the font size.
row
The number of rows of this <grut-text> element. The height of <grut-text> will be computed with this value and the font size.
font
The font of this <grut-text> element.
font-size
The font size of this <grut-text> element.
color
The text color of this <grut-text> element. The default is "white".
background-color
The background color of this window. The default is "black".
window-width
The width of this window.
window-height
The height of this window.
resizable?
Whether this window is resizable or not. The default is #t.
fit
How to fit the size and position of this <grut-text> element to this window. The available values are the same as grut-canvas-window.
scrollbar?
Whether the vertical scrollbar is shown or not. The default is #f (the scroll bar is hidden). The horizontal scrollbar is always hidden.
padding
The padding of this this <grut-text> element.
(grut-text+canvas-window width height :key text-id canvas-id title column row font font-size color background-color window-width window-height resizable? fit scrollbar? margin padding
Creates <grv-window> which has a <grut-text> element and a <canvas> element. width and height are the resolution of the canvas.
text-id
The element ID of this <grut-text> element. The default is "text-console".
You can specify a list of IDs to make multiple layered text elements like grut-text-window.
canvas-id
The element ID of this <canvas> element. The default is "canvas". You can specify a list of IDs to make multiple layered canvases like grut-canvas-window.
title
The title of this window.
column
The number of columns of this <grut-text> element. The width of <grut-text> will be computed with this value and the font size.
row
The number of rows of this <grut-text> element. The height of <grut-text> will be computed with this value and the font size.
font
The font of this <grut-text> element.
font-size
The font size of this <grut-text> element.
color
The text color of this <grut-text> element. The default is "white".
background-color
The background color of this window. The default is "black".
window-width
The width of this window.
window-height
The height of this window.
resizable?
Whether this window is resizable or not. The default is #t.
fit
How to fit the size and position of this <grut-text> and <canvas> element to this window. The available values are the same as grut-canvas-window.
scrollbar?
Whether the vertical scrollbar is shown or not. The default is #f (the scroll bar is hidden). The horizontal scrollbar is always hidden.
margin
The margin of this <canvas> element.
padding
The padding of this this <grut-text> element.

Audio

(play-mml track mml ...)
Plays music that is described in mml (Music Macro Language). track is a keyword in which the music is played. track and mml is a pair, and you can specify multiple track and mml pairs (for example, (play-mml :track1 '(c d e) :track2 '(e f g))).

The Music Macro Language is a list of these elements.

note symbol (c, d, e, f, g, a, b and qualifiers)
c, d, e, f, g, a and b correspond to a scale. You can concatitate them to describe a chord. For example, 'ceg means Cmajor. + and - means sharp and flat. 'c+ is "C sharp", and 'b- means "B flat".

You can add a number and a dot after the note symbol to describe the length. 'c4 means a quarter note, and 'c8. means dotted eighth note. If the length is omitted, the default length is used.

The notes can be concatinated with & for slur and tie like 'c&c and 'c&d.
'rlength
Rest. 'r4 means a quarter rest. If length is omitted, the default length is used.
'xlength
Noise.
:tempo bpm
'tbpm
Tempo. bpm can't be omitted.
:length bpm
'llength
Changes the default length of notes.
:octave bpm
'ooctave
Changes the octave.
'>
Increases the octave.
'<
Decreases the octave.
:volume volume
'vvolume
Changes the volume. volume must be a number from 0 to 1.
:stereo-pan pan
'ppan
Controls a pan. pan is a number from -1 to 1, -1 means full left pan, and 1 means full right pan.
:gate/step ratio
'qratio
Changes a ratio of gate time (the sound is actually sounded) and the sound length. ratio is a number from 0 to 1. The default is 7/8.
:wave-form wave-type
:wave-form (cosine-terms sine-terms)
Changes the wave shape of sounds. You can specify the shape with a symbol or cosine and sine-terms of the wave.
The wave type symbol is 'sine, 'square, 'sawtooth or 'triangle.
cosine-terms and sine-terms must be <f32vector>, and their lengths must be equal.
:adsr (attack decay sustain release)
Specifies the envelope in ADSR. attack, decay and release are time in second. sustain is a volume level from 0 to 1.
:detune cents
Specifies detuning in cents.
(beep frequency [duration])
Generates a sound of frequency and duration in second. If duration is omitted, the default length by :length is used.
(sound audio-buffer [start end duration])
Generates a sound audio-buffer. If start is specified, the sound loops from start (in second) to end (the default end is the end of the sound). duration (in second) specifies the length of the looped sound.
(compile-mml mml)
Compiles mml. You can pass the returned value to play-music to play it.
(play-music track music ...)
Plays the compiled music object.
(resume-track track ...)
Resumes track, which is paused.
(resume-all-tracks)
Resumes all tracks.
(pause-track track ...)
Pauses track.
(pause-all-tracks)
Pauses all tracks.
(stop-track track ...)
Stops playing the specified track.
(stop-all-tracks)
Stops playing all tracks.
(wait-track track ...)
Waits until the playing finishes in the specified track.
(wait-all-tracks)
Waits until the playing finishes in all tracks.
(play-beep frequency length :key wave-form volume)
Plays a sound of frequency and length. length is in second. The default wave-form is 'sine.
(play-sound url :key start end duration volume)
(play-sound audio-buffer :key start end duration volume)
Plays a sound of the specified audio-buffer or the downloaded data from url. You can loop the sound from start to end in duration.
(load-audio-buffer url :key on-error)
Loads audio data from url, and returns audio-buffer object. You can play it with play-sound or the MML command (sound audio-buffer ...).

The keyword argument :on-error can be a keyword :error (default) or #f. If it's the former, an error is signaled when the audio data can't be loaded from the URL. If it's the latter, load-audio-buffer just returns #f.

Clipboard

(copy-text-to-clipboard text)
Copies text into the clipboard.

Speech

<speech-synthesis-voice> represents a voice for the speech synthesis (it is identical to SpeechSynthesisVoice in JavaScript). The class has these slots.

default (boolean)
Whether the voice is the defalut voice or not.
lang (string)
The BCP 47 language tag of this voice.
local-service (boolean)
Whether the voice is supplied by a local speech synthesizer service or not.
name (string)
The human-readable name of this voice.
voice-uri (string)
The type of URI and location of the speech synthesis service for this voice.

These functions are provided for Speech Synthesis.

(query-all-voices :key lang name default local-service voice-uri wait?)
Returns a list of voices that matches the specified conditions. The conditions can be specified with lang name default local-service voice-uri. They are associated with the slots of <speech-synthesis-voice>.

If the slot is boolean, the associated keyword parameter can takes a boolean value. The voices that matches the keyword parameter value will be returned.

If the slot is string, the associated keyword parameter can take a string or a regular expression. The voices whose parameter contains the string or matches the regular expression will be returned.

wait? (default is #t)
Waits for the background voice loading. Some browsers loads voices in background, so SpeechSynthesis.getVoices() in JavaScript may not return any voices until the load is completed. If wait? is #t, this function waits until the getVoices() method returns voices. If no voices are available, query-all-voices will not be returned.
(query-voice :key lang name default local-service voice-uri wait?)
Returns one of the voices that matches the specified conditions. The keyword parameters are the same as query-all-voices.
(speak text :key voice)
Speaks text with voice. If voice is omitted, the default voice will be used.
(pause-speech)
Pauses the current speech.
(resume-speech)
Resumes the paused speech.
(cancel-speech)
Cancels the current speech.

Text console

graviton.grut module provides <grut-text> element that represents a text console. You can input a text using line edit and output a text. You can also control the text console using ANSI escape sequence.

<grut-text> is a <jsobject>, and it also inherits <virtual-output-port>. You can write a text to it, so that the text will be displayed in the text console element (for example, (format text-console "Hello, world")).

<grut-text> can accept generic functions of text.console.

html:grut-text
Constructs a <grut-text> element. You can use it as a part of HTML document tree by text.html-lite module.
(set-line-style! text-console row style value)
Sets the HTML style of the line row to value.
(scroll-up text-console :optional n)
Scrolls up text-console by n lines. The default n is 1.
(scroll-down text-console :optional n)
Scrolls down text-console by n lines. The default n is 1.
(scroll-to text-console row :optional align-to-top)
Scrolls to the line row. If align-to-top is #t, the line row will be aligned at the top. Otherwise, the line will be aligned at the bottom. The default value of align-to-top is #t.
(compute-character-position&size text-console column row)
Returns the rectangle in the viewport which contains the character at (column, row). This function returns 4 values (x, y, width and height).
(get-input-text text-console :optional wait?)
Returns a string in the input queue of text-console. If the input queue is empty and wait? is #f (default), #f is returned.

If wait? is #t, this function waits for an input event if the queue is empty.

Line Edit

(make-keymap :optional parent)
Makes a keymap. If a parent keymap is specified, the newly created keymap inherits the parent.
(global-keymap)
Returns the global keymap, which is effective at first.
(bind-key keymap key action :key use-clipboard-text?)
Assigns action to key in keymap.

key is a key value of KeyboardEvent. If you want to specify modifier keys, Alt, Meta, Ctrl and Shift, you need to add prefixes "A-", "M-", "C-" and "S-" in this order. For example, if you want to specify "Ctrl+Shift+PageDown", the key is "C-S-PageDown".

You can also specify continuous keys like "Ctrl+X Ctrl+S" in Emacs. In this case, the key is "C-x C-s".

action must be a string or <procedure>. If action is a string, the string will be inserted if the key is pressed. If action is a procedure, the procedure will be called with <input-context>. You can get information of line editing from the context.

If you want to use a clipboard text, you need to specify #t to :use-clipboard-text? keyword parameter. You can get the clipboard text from the <input-context>.

(switch-keymap input-context keymap)
Switches the current keymap to keymap.
(read-text/edit text-console :key prompt keymap input-continues initial-text cursor-column cursor-row on-change on-input data-alist)
Reads a text that the user inputs on text-console. The user can use line edit in the input.
prompt
The prompt string of this input. The default is an empty string.
keymap
The keymap used in this input.
input-continues
The procedure that determines whether the input continues or finishes. This procedure is called with input-context when Enter is pressed. If it returns #f, the input finishes and read-text/edit returns the input text. Otherwise, the input continues.
If input-continues is #f (default), the input finishes when Enter is pressed. If you don't need multiple line edit, you don't need to specify input-continues.
initial-text
The ininitial text of this input.
cursor-column
The column number of the initial cursor position.
cursor-column
The row number of the initial cursor position.
on-change
The procedure that is called with <input-context> when the input text is changed.
on-input
The procedure that is called with <input-context> when the user types any keys.
data-alist
The data that will be passed to <input-context>. You can get the data with input-context-data-get and modify it with input-context-data-put!.
(clipboard-text input-context)
Returns the text in the clipboard. If no text is available, #f is returned.
(input-context-text-line input-context :optional row)
Returns the line of the input text at line row. The default row is the current line.
(input-context-text-content input-context)
Returns the whole text of the input text.
(input-context-data-get input-context key :optional default)
Searches key in input-context, and returns its value if found. Otherwise, returns default (the default is #f).
(input-context-data-put! input-context key value)
Puts key with value in input-context.
(input-context-data-delete! input-context key)
Deletes key in input-context.
(edit:backward-delete-char input-context)
Deletes the previous character.
(edit:beginning-of-edit-area input-context)
Moves the cursor to the beginning of the input area.
(edit:beginning-of-line input-context)
Moves the cursor to the beginning of the current line.
(edit:cancel-edit input-context)
Cancels the input text. read-text/edit returns #f.
(edit:copy input-context)
Copies the selected text into the clipboard.
(edit:cut input-context)
Copies the selected text into the clipboard, and deletes the text.
(edit:delete-char input-context)
Deletes the current character.
(edit:end-of-edit-area input-context)
Moves the cursor to the end of the input area.
(edit:end-of-line input-context)
Moves the cursor to the end of the current line.
(edit:forward-char input-context)
Moves the cursor one character forward.
(edit:insert-string input-context string)
Inserts string at the current cursor position.
(edit:newline-or-commit input-context)
Finishes the input, and returns the input text if input-continues that is specified by read-text/edit returns #f. Otherwise, inserts a newline.
(edit:next-line input-context)
Moves the cursor vertically down one line.
(edit:page-up input-context)
Moves the cursor vertically up one page.
(edit:page-down input-context)
Moves the cursor vertically down one page.
(edit:paste input-context)
Pastes a text in the clipboard.
(edit:previous-char input-context)
Moves the cursor one character backward.
(edit:previous-line input-context)
Moves the cursor vertically up one line.
(edit:select-all input-context)
Selects all text in the current input area.

JavaScript FFI and JSiSE (JavaScript in S-expression)

Graviton provides S-expression JavaScript (JSiSE) syntax like CiSE of Gauche. You can use it in the macros described below.

Basically, (func arg1 arg2 ...) in JSiSE is translated to func(arg1, arg2, ...) in JavaScript. A symbol in JSiSE is an identifier or a variable name in JavaScript. A dot "." in a JSiSE symbol has a special meaning, which means property reference of an object. It is the same as JavaScript. So (console.log "Hello, world") is console.log("Hello, world"), and (set! foo.bar 1) is foo.bar=1.

(define-jsvar var [value])
Defines a JavaScript variable var in the current JavaScript module. This JavaScript module is created implicitly per Gauche module. This variable var can't be seen from other modules. If value is omitted, the variable is initialized with undefined.
(define-jsfn (func args ...) body ...)
Defines a JavaScript function func in the current JavaScript module.
(inline-js body ...)
Embeds JavaScript code in the toplevel of the current JavaScript module. This code runs when the module is loaded.
(import-js module-path :as name)
(import-js module-path [:only (only-export ...)] [:rename ((rename-export alias) ...)])
Imports the JavaScript module from module-path into the current module.

The former statement is translated to import * as name from module-path, it will import all the contents of the module as name.

The latter statement is translated to import {only-export, rename-export as alias, ...} from module-path, it will import the selected contents of the module. In the latter statement, you need to specify at least one of :only or :rename keyword.

(jslet ((jsvar [value]) ...) body ...)
Embeds JavaScript code. value is an object in Scheme world. It is transferred to JavaScript world and bound to jsvar. jslet doesn't return a meaningful value. Currently, it returns #<undef>.

If value is omitted, an object which is referenced by the same name jsvar in Scheme world, is bound.

In run-time, jslet sends a request to the client (browser or graviton-player) to execute body .... But the request is buffered until the current continuation finishes. If you want to send the request immediately, call (asleep 0).

(jslet/async ((jsvar [value]) ...) body ...)
(jslet/await ((jsvar [value]) ...) body ...)
Basically, jslet/async and jslet/await are the same as jslet, but they can return values. You can use (respond val ...) macro to return values to Scheme world.

jslet/async returns <grv-promise> object immediately, and you need to use (await promise) to get the values.

jslet/await is identical to (await (jslet/async ...)).

await doesn't block the current thread. The continuation is called after the JavaScript code returns values. Until then, the thread can execute other code (event handler or other waiting continuations, etc.).

Like jslet, the request to execute body ... is buffered. Calling await will send the buffered requests because it finishes the current continuation. jslet/await always calls await, so the buffered requests are sent at jslet/await location.

The following sections explain the syntax of JSiSE.

Literals

  • #t means true.
  • #f means false.
  • null means null.
  • undefined or #<undef> means undefined.
  • #(v0 v1 v2 ...) means a vector [v0, v1, v2, ...].
  • "string" means a string.

Vector

(vector val ...)
It means a vector [val, ...].
(vector-ref vec i)
It means vec[i].
(vector-set! vec i val)
It means vec[i]=val.

Object

(object (key . val) ...)
It means a JavaScript object {key: val, ...}. key must be a string.
(new class args ...)
(make class args ...)
It means new class(args, ...).
(~ expr attr ...)
(ref expr attr ...)
It means a property reference. If the attribute attr is a symbol, it is translated to (expr).attr. Otherwise, it is translated to (expr)[attr]. You can pass multiple attr for reference chain.

Control flow

(if expr then-body [else-body])
It means if (expr) { then-body } else { else-body }. This if macro is available in an expression. In that case, the if is tralslated to (expr) ? (then-body) : (else-body).
(cond (expr then ...) ... [(else else-body ...)])
It is translated to continuous if-else statements. This cond macro is available in an expression like if macro.
(dotimes (var num-expr) body ...)
It repeats body ... for a number of times num-expr. var indicates the number of the loop (starts from 0).
(when expr body ...)
It means if (expr) { body... }.
(unless expr body ...)
It means if (!expr) { body ...}.
(case key ((data0 data1 ...) body ...) [(else else-body ...)])
It means switch (key) { case data0: case data1: ... { body ... } break; default: else-body ...}.
(dotimes (var num-expr) body ...)
It is equivalent to Scheme's dotimes. It repeats body for a number of times.
(while expr body ...)
It means while (expr){body ...}.
(for-each proc coll)
It means coll.forEach(proc).
(return [val])
It means return [val].
(begin body ...)
It means { body ... }.

Local variables definition

(let ((var expr) ...) body ...)
(let* ((var expr) ...) body ...)
let and let* are equivalent to Scheme's let and let*. They creates a local scope where var ... are bound to the value of expr ..., then evaluates body ....
(let1 var expr body ...)
let1 is a convenient macro for only one variable. It is the same as (let ((var expr)) body ...).
(rlet1 var expr body ...)
rlet1 binds the value of expr to var, then evaluates body .... After that, it returns var.

Assignment

(set! var expr)
It means var=expr.
(inc! var [delta])
It means var+=delta. The default value of delta is 1.
(dec! var [delta])
It means var-=delta. The default value of delta is 1.

Expression operators

  • (not v) means !v.

  • (or v0 v1 ...) means v0 || v1 || ....

  • (and v0 v1 ...) means v0 && v1 && ....

  • (lognot v) means ~v.

  • (logior v0 v1 ...) means v0 | v1 | ....

  • (logand v0 v1 ...) means v0 & v1 & ....

  • (logxor v0 v1 ...) means v0 ^ v1 ^ ....

  • (equal? v0 v1 ...) means v0 === v1 === ....

  • (< v0 v1 ...) means v0 < v1 < ....

  • (<= v0 v1 ...) means v0 <= v1 <= ....

  • (> v0 v1 ...) means v0 > v1 > ....

  • (>= v0 v1 ...) means v0 >= v1 >= ....

  • (+ v0 v1 ...) means v0 + v1 + ....

  • (- v) means -v.

  • (- v0 v1 ...) means v0 - v1 - ....

  • (* v0 v1 ...) means v0 * v1 * ....

  • (/ v0 v1 ...) means v0 / v1 / ....

  • (modulo v0 v1) means v0 % v1.

  • (ash n count) means << (if count is positive or zero) or >> (if count is negative).

  • (pre++ var) means ++var.

  • (pre-- var) means --var.

  • (post++ var) means var++.

  • (post-- var) means var--.

  • (lambda (arg ...) body ...) means function (arg, ...) { body ... }.

  • (is-a? expr class) means expr instanceof class.

Returning values to Scheme

(respond val ...)
It returns the values val ... to Scheme world.

Global variable definition

(define var val)
(define (func args ...) body ...)
They are translated to let var = val and let func = function(args, ...) { body ... }.
(import var name)
(import (var ...) name)
They translated to const {var, ...} = require(name).

NOTE: define and import are intended to define a global object in inline-js.