GLRepl is a Max Repl (Read/Execute/Print/Loop) environment based on the
excellent th.gl.texteditor. It consists of two objects [tw.gl.repl]
and
[tw.gl.repl.dynamic-size-helper]
.
At it's core this is the same idea as th.gl.texteditor but the way in which functions can be attached to keys now significantly extends what it is possible to do. There is a fundamental philosophical difference between the idea of having a text buffer repl which performs actions on run/execute and a program where additionally every key press triggers a specific function. It means there are subtle differences between this and th.gl.texteditor which are important to be aware of. For example when you load a text file into th.gl.texteditor it just fills the buffer, in tw.gl.repl it replays the keystrokes back through the input processing. This means that any function which is attached to the individual keypress will be executed again. In it's usual configuration this means that the text is added to the text buffer, but it does not necessarily hold that this is true in every possible configuration. It would be possible to attach functions to keypresses which maintain state for other parts of an application, or which trigger messages to be output immediately etc. This means you should think about where you put your functionality, does it need to be in the repl itself, ie should it be triggered every time the keypresses are played back? or does it need to be some routing and handling in max? Further to this the repl introduces the concept of output formatters, these can be attached to the repl and then used in the configuration file to alter the output in some way. This allows you to format text easily for whatever you are hooking the repl up for, for example concatenating the output into a single line, or checking that it has balanced brances, or ensuring whitespace is in a regular format. However it also means that it's possible to, for example, have a short dsl for the repl, which is expanded to a full DSL of the thing you wish to interface with. This is useful if you need to interact with a verbose javascript but don't want to do a lot of typing.
TLDR not only is it possible to output the contents of the repl buffer for processing in max, but it's possible to attach any function to a keypress in the repl, which can in turn do things including generate messages for output. The text you input can also be mutated on run/execute so that something different is output from the repl.
Simple use cases for the repl can be handled entirely in configuration, and more
complex use cases can be easily managed by including a user-repl.js
file
inside your project in which you can further customize behaviour by attaching
your own custom functions to keypresses or your own custom formatters for output
message handling. Read on for more about this.
See the patch in the extras menu for a few examples of how you might use the repl.
You should install this inside your Max packages directory, in a folder called GLRepl
,
it should then be available in max after a restart.
See help files for some ideas on what you might do with it!
1. download a release from the github release page for this project
2. unzip and place in Max Searchpath (eg. MacOS ~/Documents/Max 8/Packages)
3. restart Max8
If you want to git clone the repo you will need to have npm
and tsc
installed
as the compiled sources are not included in the repo.
cd ~/Documents/Max\ 8/Packages
git clone https://github.com/twhiston/tw.gl.repl.git
cd GLRepl/javascript
npm install && npm compile
//start Max8
4. Go to the extras menu and open the "GLRepl Overview" patch
All source files loaded by max are in the dist
folder and the typescript which
it is compiled from is found in src
. Unless you have a more complex project in
mind you probably don't need to care about this and can use the config file and user-repl.js
to extend the functionality of the repl.
By default executing the code in the repl will run a series of formatters and output the resulting text from outlet 0. This allows you to write livecoding style commands in the repl, ensure they are formatted as needed, and then output them for further routing and processing in max.
It's undeniably the most useful to have a repl that you can dynamically resize
and to this end a helper object is included. See the help file for information
on how to connect this, or hover the inlets and outlets in max. The scaling fits
some window sizes better than others, and sometimes it might unavoidably break a
boundary, you should just resize the window in a way that sorts this out.
You can also send a scale 1.
value to the object, which the current scaling will
be multiplied by. No guarantee that the current scaling works super well with every
font either!
Basic configuration of your repl can be achieved by loading a replkeys.json
file to reconfigure it. This file is an object
The config is in the following form:
{
"settings":{
"keypressProcessor": {
"overrideAlphaNum": true
}
}
"bindings": [
{
"id": "execute",
"asciiCode": 2044,
"functions": [
"return 'run'"
]
},
{
"id": "backspace",
"asciiCode": -7,
"functions": [
"ctx.backSpace()"
]
},
{
"id": "customSpace",
"asciiCode": -2,
"functions": [
"myCustomFunction"
]
}
]
}
Settings allow you to set the value of some repl settings instead of settings them through messages or in code. All currently available settings are as follows:
"settings": {
"repl": {
"INDENTATION": 4,
"CMNT": "//"
},
"keypressProcessor": {
"overrideAlphaNum": true
},
"textbuffer": {
"formatters": [
"whitespace",
"bracebalanced",
"singleline",
"commentremover"
//can also include your own custom formatters here
]
}
}
Bindings are an array of of objects which bind a key number to a function. In
contrast to th.gl.editor there are no internal functions, so everything is
defined in this file and the user can override anything. As you can see there
are a number of ways to define the functions that are called, and it is possible
to call multiple functions with a single key. Functions can be defined as a
function body in text (which will be wrapped
new Function('k', 'ctx', funcString)
), it can be a function from whatever context
is passed in (in the case of this application it is an instance of REPLManager
),
or it can be a reference to a custom function.
There is one "special" keycode which is not defined in config, this is the binding
for 'ignore_keys'. This is hardcoded to option+d
which is keycode 8706. This needs
to be handled outside of the javascript because you want to be able to re-enable
the keys. You can change this binding by sending the message ignore_keys_id
and
the keycode id that you want.
You can create a simple key binding to output a message when a key is pressed with the following configuration
{
"id": "execute",
"asciiCode": 2044,
"functions": [
"return 'run'"
]
}
Each of the entries in functions
will be wrapped in a
new Function('k', 'ctx', funcString)
and will be executed on keypress. This
allows us to perform simple actions such as returning custom messages which we
can process further in max easily.
Because the functions called have the signature ('k', 'ctx')
functions we create in
config will always contain the value of the key that was pressed in k
. The ctx
parameter however will contain an instance of REPLManager, which means that its functions
and all the functions of the subclasses are available here. This allows you to create very
complex functionality in just the config file. The shortkey to replace a line of
text in the buffer with the pastebin is an exaxple of this
{
"id": "replaceLine-alt-p",
"asciiCode": 960,
"functions": [
"var pb = ctx.tb.pasteBinGet(); var startLine = ctx.c.line(); ctx.deleteLine(); if(ctx.c.line() < ctx.tb.length()-1){ctx.jumpLine(-1);ctx.jumpTo(1);} if(startLine === 0){ctx.jumpTo(0); ctx.newLine(); ctx.jumpTo(2); }else { ctx.newLine(); } for(var i = 0; i < pb.length; i++){for (var a = 0; a < pb[i].length; a++) {var char = pb[i].charCodeAt(a); ctx.keyPress(char)}}"
]
}
One of the ways to extend the repl further is to attach or preload your own functions
so you can tie them to a key in the config.
To make this easier the package tries to load a file called user-repl.js
, max should
load this fine if it's in your patch folder. Inside it you have access to glrepl.renderer
and glrepl.manager
, you also have access to a Dict of replkeys.json
in sKeys
. Which
will be stringified and passed into the repl on init()
Most basic usage will be something like:
//Typescript signature is actually
//const functionOne = (k: number, ctx: {}) => {
const functionOne = (k, ctx) => {
return `some message`;
};
glrepl.manager.kp.preloadFunction('doSomething', functionOne);
You can then use this in your replkeys.json
app config by binding it
to a key
{
"bindings": [
{
"id": "pushSpace",
"asciiCode": -2,
"functions": [
"doSomething"
]
}
]
}
See the examples/custom-formatter/user-repl.js
for a working example.
Alternatively if, for some reason, you want to configure it in code rather than with json you could attach the function directly.
//glrepl.manager.kp.attachFunctions(id: string, keyCode: number, funcs: Array<KeyProcessor>)
glrepl.manager.kp.attachFunctions("arbitraryName", -2, [functionOne])
which will then be run when the key is pressed. All custom function should be of
type KeyProcessor
and thus have the signature function(k: number, ctx: {})
.
Functions can return nothing or Array<string>
, these strings are treated
as messages to be output to max, so you can write routing and handling in max to
implement whatever you need. If you get an error message about prototype apply taking
an array you probably are outputting a string and not an array of strings!
Be very careful about creating JitterObjects in your custom functions or in your
code at all. When they are used outside of the top level js file they are not
freed automatically, which then results in a crash. See the bound _close
function for how this is handled for the GLRender class's destroy
method.
By default alphanumeric characters are treated with a special function which records the keypress into a text buffer for display and output. It may be the case that you don't want to do this because you want to attach specific functions to every key. You could override the default handler which will stop this function being called:
//user-repl.js in your path
glrepl.manager.kp.customAlphaNum(true);
or
"settings": {
"keypressProcessor": {
"overrideAlphaNum": true
}
}
If you instead want to just override the default handler for alpha-numerical keys
you should bind a function to keycode 127
replacing the default one (shown below)
{
"bindings": [
{
"id": "alphahandler",
"asciiCode": 127,
"functions": [
"ctx.addChar(k)"
]
}
]
}
By default there are two formatters which run on execute, firstly WhitespaceFormatter
will trim stings and ensure consistency in the whitespace character used. Secondly
BraceBalancedFormatter
will check that our output has fully balanced braces, or
it will throw an exception. This exception is handled in the repl and will be printed
to the max console. This formatter is extremely useful if you are outputting some
kind of dsl, and need to ensure it is formatted correctly. However it's easy to turn
it off by simply removing it from the config of the repl like so
"textbuffer": {
"formatters": [
"whitespace",
]
}
If you need to add additional formatters you can add them in your user-repl.js
by implementing a TextFormatter and preloading it. It can then be referenced in
your repl config.
// This example is in typescript for clarity, and user-repl.js needs
// to be in the type of archaic javascript that max understands but
// hopefully you get the idea. To create a lot of extensions for the
// repl it's recommended to look into using typescript, transpiling and
// generating your user-repl.js file.
class UppercaseFormatter implements TextFormatter {
id: string = "uppercase"
format(strArr: Array<string>, ctx: {}): Array<string> {
// Example implementation that returns all strings in uppercase
return strArr.map(str => str.toUpperCase());
}
}
glrepl.manager.preloadFormatter(new UppercaseFormatter);
//include via repl json config: {"settings"{"textbuffer": {"formatters": ["uppercase"]}}}
Always prefer preloading over setting formatters directly as failure to do so will result in issues when the config is loaded, as this is the point at which formatters are resolved and added to the TextBuffer.
To see a full example of a pure javascript text formatter implementation check out
the Custom Formatter example from the Max Extras menu tw.gl.repl.overview
patch.
Writing files will save the contents of the buffer into a text file.
IMPORTANT NOTE: reading files does not just fill the buffer with the text,
because the possibility of attaching functions to each key means that
progressive keypresses can build up application state, when a file is loaded
it is played back as individual keypresses. Because of this you need to ensure
that your config handles both the max and filesystem keycodes for things like
spaces or new lines. You can see this in the default configuration provided.
If you need to work out what keycode a system specific keypress is look inside
the tw.gl.repl object and the messagebox connected to the output of p quickKey
.
Usually tw.gl.repl
is just calculating the scaling values from the dimensions
that you give it in the arguments. However there are occasions where it may be
beneficial to have dynamic scaling. To achieve this you can use the
tw.gl.repl.dynamic-size-helper
object together with jit.world
and
tw.gl.repl
. Add the object, connect inlet 1 to outlet 2 of jit.world
connect
inlet 2 to outlet 3 of jit.world
, connect outlet 1 to the inlet of jit.world
and connect outlet 2 to the inlet of tw.gl.repl
. With this in place the text
should scale fairly nicely with window resizing. It might still be a bit weird
at really strange aspect ratios.
See the help file for the object for more info.
Key differences from th.gl.texteditor are listed below. Other than the total refactoring there are some subtle, and not so subtle differences that mean it's a little work to migrate from one to the other.
- Different shortkey.json format, and also includes application settings
- Different concept of file handling, file contents is "played back" into the repl
- No internal functions, everything can be user defined in code or config and attached to a key
- No MAX_CHARS buffer width restriction
- No buffer length restrictions
- output_matrix 1 will not stop commands being output from the first outlet,
it will just output the
jit_matrix name
command additionally! - ephemeral_mode to clear the buffer/line after every run/execute
- adds some additional methods and arguments
- Helper object for dynamic window resizing
- routepass object in tw.gl.repl.maxpatch is generated in js on
init
. - Autogenerated max bindings in js, routepass object and help xml file
- All js file handling
- Written in modern modular typescript code and then transpiled to es3 for max's ancient engine
- Extremely flexible to extend
- Full set of tests
Practically most patches using th.gl.texteditor
can use tw.gl.repl
as a drop
in replacement, though if you use a custom config you will need to adapt it to
the different format used here. In your max patch if th.gl.texteditor
's output
was connected to a fromsymbol
or iter
you can also delete these
We transpile so we can use modern js. See: https://cycling74.com/forums/any-plans-to-update-support-for-recent-versions-of-js#reply-58ed21d5c2991221d9ccad8c
Although the runtime needs no external libraries, you will need to npm install
inside the javascript folder to develop this code, as it's all written in typescript
and needs to be transpiled.
npm run compile
will render the max compatible javascript from our typescript and
generate the tw.gl.repl.js
file which is the core of our repl. It will also generate
the max help xml file because this should match the functions we have exposed in
the main repl file, and this might change if we add annotations to functions or methods.
Testing is done with the ava
framework. If you are going to add a new feature
and contribute it back (please do!) then you'll need to write tests for it as well.
The code here has pretty good test coverage so look at the moduleName.test.ts
files
for lots of examples.
npm run test
will run all the tests and output coverage. npm run report
will
generate an html report you can use to see where you are missing test coverage.
Github pipelines will run on push, these run all tests and output coverage and also compile the code.
The entrypoint into the code for max is in an autogenerated file, this makes binding
existing functions to the max interface easier as you just need to annotate the code
and run the generator. Functions are annotated like
@maxMspBinding({ draw: true, functionName: 'cursor' })
see MaxBindings/MaxBinding.ts
for a full list of options. You can annotate the class as well, which is useful
for the eg. instanceName
field.
Although there are only a few options available to the binding the processor enriches the content with various other bits of metadata which can be used in template rendering. See the templates.
Mostly you won't need to touch this stuff, as you can extend the repl using replkeys.json
and/or user-repl.js
for most simple use cases. But if you want to build your own
more complex repl object you will need to recompile and generate the js code.
There is a helper patch included editor-development.maxpat
which has a helpful
simple setup which you can use to help with developing.
Releases are generated using release-it
. Any commit into main will produce a release
and any release without breaking changes included will be a minor version bump.
Development releases are created as needed by manually running npm run release-beta-major/minor
in the javascript folder. You must have a clean checkout of the develop branch
to do this.
The changlog is updated automatically on release and thus commits should be in the conventional commits format so they can be included. Commitlint will enforce this for commit messages and on merges in github actions.
Lefthook is used for local commit linting. Note that because our package.json
is not in our root folder, since this is not a pure node project it is expected
that you have your own global install of lefthook npm install -g lefthook
and
that you manually run lefthook install
in the root after cloning the repo.
It's worth noting that if you use vscode on OSXyou might have problems with these hooks silently failing if the binaries are not found. vscode seems to always use bash for git operations, although the default OSX shell is zsh. Therefore you might need some specific bash config around node, especially if you are using NVM. For me it looks like this:
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
eval "$(/opt/homebrew/bin/brew shellenv)" # setup homebrew paths for global installs of linting tools
export NODE_PATH=$NODE_PATH:`npm root -g` # get the correct node path
The GNU Lesser General Public License v.3
The artistic and aesthetic output of the software in the examples is licensed under: Creative Commons Attribution-ShareAlike 4.0 International License
(c) Tom Whiston 2023
The origin of this project is a refactoring of th.gl.texteditor (c) Timo Hoogland 2020