Runing embed programming language in simple graphical environment, for kids and newbies, using raylib as small game engine to draw.
Currently supports Lua (.lua / .ls).
It also should have editing tool inside that environment, console output and input, work in same graphical window.
playing sound using mmf code.
It is More simulating old computer, but with modern languages and graphic.
tyro [<script>] [--workpath=<dir>] [<options>]
Running a .lua/.ls file executes it in a graphical environment. Without a
script argument Tyro starts an interactive console session.
| Option | Description |
|---|---|
--help, -h |
Show the help page and exit |
--list |
List the supported programming languages and exit |
--console, -c |
Force a command prompt / console output |
--debug, -d |
Enable debug logging |
--lint, -l |
Syntax-check the script with Lua and exit without running it (nothing is executed, no window opens) |
--main, -m |
Legacy compatibility alias; scripts always run on a worker thread with main-thread dispatch |
--exit, -x |
Exit automatically after the script finishes |
--execute, -e |
Alias for --exit (run the script, then exit) |
--show=true/false, -s |
Force the window visible/hidden; default keeps script-controlled behavior |
--workpath=<path> |
Workspace directory (defaults to the executable location) |
| Status | Meaning |
|---|---|
0 |
Success (also --help/--list, and interactive sessions closed normally) |
1 |
Lua runtime error while running with --exit/--execute, or --lint found syntax errors |
2 |
Invalid command line usage: unknown option, --lint without a script file, or an invalid --show value |
canvas.text(10, 30, 'Printing text test')
i = 1000000
canvas.color = colors.black
canvas.line(0, 100, canvas.width, 100)
while i > 0 do
c = math.random(3, colors.count)
canvas.color = colors[c]
r = math.random(5, 20) --size of circle
x = math.random(640)
y = math.random(480)
canvas.circle(x, y, r, true)
r = math.random(5, 20) --size of circle
x = math.random(640)
y = math.random(480)
canvas.rectangle(x, y, r, r, true)
sleep(10)
i = i - 1
endKeyboard and mouse input can be queried from script. Key and button names are strings that map to RayLib key/button enums — they work identically in Lua and PascalScript.
| Function | Description | Examples |
|---|---|---|
iskeypressed(key) |
True once when the key transitions from released to pressed | "space", "w", "f1" |
iskeydown(key) |
True every frame while the key is held | "ctrl", "up", "a" |
Supported key names: letters a-z, digits 0-9, space, enter, tab,
escape, backspace, delete, insert, up, down, left, right,
f1-f12, shift, ctrl, alt.
window.show()
while true do
if iskeydown("w") then print("W is held") end
if iskeypressed("space") then print("Space pressed!") end
sleep(16)
end| Function | Description | Examples |
|---|---|---|
mousex() |
Current mouse X position | — |
mousey() |
Current mouse Y position | — |
ismousepressed(button) |
True once when a mouse button is pressed | "left", "right", "middle" |
window.show()
while true do
if ismousepressed("left") then
canvas.circle(mousex(), mousey(), 5, true)
end
sleep(10)
endWhen the graphical console is visible, you can type commands at the > prompt.
The following commands are built in:
| Command | Description |
|---|---|
dir, list, ls |
List files in the current directory |
clear, cls |
Clear the console output |
help, ? |
Show available commands |
exit, quit |
Hide the console and stop the engine |
ESC |
Hide the console (keybinding) |
Example:
tyro demos/terminal_demo.lua
# then type "dir" at the console prompt
Holding a key auto-repeats it at the prompt: typed characters, BACKSPACE,
DELETE, the arrow keys and HOME/END keep repeating after a short delay.
ENTER, ESC and CTRL shortcuts stay one-shot.
Text at the prompt can be selected with SHIFT+LEFT/RIGHT/HOME/END (a
plain arrow press jumps to the edge of the selection), with CTRL+A, or by
dragging with the mouse. Typing, BACKSPACE, DELETE or pasting replaces the
selection, and it is copied/pasted with CTRL+C / CTRL+V or with the
traditional console keys CTRL+INSERT / SHIFT+INSERT.
| Function | Description |
|---|---|
console.show([w, h]) |
Show the console (optionally sized in characters) |
console.print(text) |
Print text to the console (no newline) |
console.println(text) |
Print text to the console (with newline) |
console.read([prompt]) |
Block and read a line of input from the user |
console.active |
Read-only boolean — true while the console is visible |
console.read() displays a prompt on the console and blocks the script until
the user types a line and presses Enter. It returns the typed string.
console.show()
local name = console.read("Your name? ")
println("Hello, " .. name .. "!")See demos/terminal_demo.lua and demos/console_read_demo.lua for examples.
The Sprites system manages textured images ("sprites") that the engine draws
every frame. Each sprite is created with Sprites.new, loaded with load, and
positioned with move or by setting x/y properties.
| Function | Description |
|---|---|
Sprites.new("name"?) |
Create a new sprite. Optional name registers it for lookup. |
Sprites("name") |
Look up a sprite by name (returns the sprite object or nil). |
Sprites.find("name") |
Same as above. |
| Method | Description |
|---|---|
sprite:load("image.png") |
Load a texture from file into this sprite. .aseprite/.ase files load every frame as an animation. |
sprite:show() |
Make the sprite visible (shown by default). |
sprite:hide() |
Hide the sprite from rendering. |
sprite:move(x, y) |
Set the sprite's position. |
sprite:width() |
Return the texture width in pixels. |
sprite:height() |
Return the texture height in pixels. |
sprite:play([fps]) |
Restart and play the animation. fps overrides the per-frame timings stored in the file (0 or omitted keeps them — needed because Aseprite files often have ~1 ms delays). |
sprite:stop() |
Freeze the animation on the current frame (aliased by sprite:pause()). |
sprite:framecount() |
Number of frames in the loaded .aseprite animation. |
| Property | Type | Description |
|---|---|---|
sprite.x |
number |
X position (read/write). |
sprite.y |
number |
Y position (read/write). |
sprite.angle |
number |
Rotation in degrees (read/write). |
sprite.scale |
number |
Scale factor, 1.0 = original size (read/write). |
sprite.visible |
boolean |
Whether the sprite is drawn (read/write). |
sprite.frames |
number |
Number of animation frames (read-only). |
sprite.frame |
number |
Current animation frame, 0-based (read/write). |
sprite.playing |
boolean |
Whether the animation is running (read/write). |
sprite.speed |
number |
Animation speed in frames-per-second; 0 = use the file timings (read/write). |
sprite.looping |
boolean |
Whether the animation loops when it reaches the last frame (read/write). |
Image files are searched in the script directory, the workspace sprites/
folder, and the current directory.
window.show(640, 480)
canvas.color(colors.black)
canvas.clear()
mysprite1 = Sprites.new("player")
mysprite1.load("richard-say.png")
mysprite1.show()
mysprite1.move(100, 200)
mysprite2 = Sprites.new()
mysprite2.load("richard-say.png")
mysprite2.move(300, 100)
mysprite2.angle = 45
mysprite2.scale = 0.5
-- Look up by name
local p = Sprites("player")
println("Sprite size: " .. p.width() .. "x" .. p.height())
local a = 0
while true do
canvas.color(colors.black)
canvas.clear()
-- Engine draws all sprites automatically; just update properties
mysprite1.move(mousex(), mousey())
mysprite1.angle = a
a = a + 1
if a >= 360 then a = 0 end
sleep(16)
endPost-processing screen shaders are controlled with the shader table. They are
built-in and need no files. Assign properties the usual way:
shader.effect = "water" -- pick the effect
shader.value = 0.5 -- 0..1 effect parameter
shader.area = {x, y, w, h} -- restrict the effect to a rectangle| Property | Description |
|---|---|
shader.effect |
"water", "glow", "gray", "sepia", "invert", "vignette", "pixelate" or "none" |
shader.value |
0..1 effect parameter (see per-effect meaning below) |
shader.area |
Rectangle {x, y, w, h} in canvas pixels (y from the top) where the effect applies; outside it the canvas is unchanged |
shader.load(file) |
Load a custom fragment shader from a GLSL file and activate it (see below) |
| Effect | What it does | value meaning (default) |
|---|---|---|
"water" |
Wavy water with foam across the lower region | water surface height (0.34) |
"glow" |
Soft glow around bright pixels (suns, bulbs, lasers) | height of the glowing region from the bottom (1.0 = full) |
"gray" |
Grayscale | gray strength (1.0) |
"sepia" |
Old-photo look | sepia strength (1.0) |
"invert" |
Inverted colors | inversion strength (1.0) |
"vignette" |
Darkened corners | vignette amount (0.5) |
"pixelate" |
Chunky pixel blocks | block size (0.06 ~ 1px) |
window.show(640, 480)
shader.effect = "glow" -- turn it on
shader.value = 0.5 -- glow on the bottom half
-- draw something bright, it will glow
canvas.color = colors.yellow
canvas.circle(320, 240, 60, true)shader.value and shader.area survive effect switches, so once set they apply
to whichever effect is active. Toggle effects and tweak the parameters at any
time:
if iskeypressed("1") then shader.effect = "water" end
if iskeypressed("2") then shader.effect = "glow" end
if iskeypressed("0") then shader.effect = "none" end
if iskeypressed("q") then shader.value = shader.value - 0.1 end
if iskeypressed("e") then shader.value = shader.value + 0.1 end
if iskeypressed("z") then shader.area = {0, canvas.height/2, canvas.width, canvas.height/2} endThe "water" effect tints and distorts the lower part of the canvas, so draw a
shore or sea bottom there if you want a sea scene. See demos/shader_demo.lua
for a full on-screen demo of every effect, value and area.
shader.load("file.frag") loads a fragment shader from a file and activates
it. The file is looked up next to the script, then in the current directory, then
in assets/ under the workspace. It must be GLSL #version 330 with the same
outputs as the built-in effects:
#version 330
uniform vec2 resolution; // canvas size in pixels
uniform float time; // elapsed seconds
uniform float value; // shader.value (0..1)
uniform vec4 area; // shader.area {x, y, w, h} (y from the bottom)
uniform sampler2D texture0; // the canvas
uniform vec4 colDiffuse; // vertex color
in vec2 fragTexCoord;
in vec4 fragColor;
out vec4 finalColor;All uniforms are optional: use the ones you need and ignore the rest. shader.value
and shader.area still control value/area. Set shader.effect = "none" to go
back to the built-in effects. See demos/custom.frag for a working example:
shader.load("custom.frag") -- activate the scanlines shader
shader.value = 0.8
shader.area = {0, 0, canvas.width, canvas.height}shake jolts the world, the way an accident or an error should feel. Every
frame the canvas and the sprites are moved by a random offset that fades out
until the time is up; the terminal and the other controls stay glued to the
window.
| Function | Description |
|---|---|
shake(ms [, power]) |
Shake the world for ms milliseconds; power is the maximum offset in pixels (10 by default) |
window.shake(ms [, power]) |
The same function on the window table |
window.shaking |
Read-only boolean — true while a shake is still running |
shake(0) |
Stop a running shake |
A new call restarts the shake, so a longer or harder one simply wins. See
demos/shake_demo.ls.
window.show(640, 480)
-- an accident: a long, hard jolt
shake(800, 40)
while cycle do
canvas.color = colors.black
canvas.rectangle(0, 0, canvas.width, canvas.height, true)
canvas.color = colors.red
canvas.circle(300, 240, 30, true)
if iskeypressed("space") then
window.shake(400) -- 400ms with the default power
end
endControls are created by class name and are owned by the control tree. Panels are
containers, so a control can be nested inside another control with
controls.parent.
panel = controls.new("panel", "", 0, 0, 240, 300)
controls.align(panel, "left")
top = controls.new("button", "Top", 0, 0, 180, 40)
controls.parent(top, panel)
controls.align(top, "top")
bottom = controls.new("button", "Bottom", 0, 0, 180, 40)
controls.parent(bottom, panel)
controls.align(bottom, "bottom")| Function | Description |
|---|---|
controls.new(class, caption, x, y, w, h, name?) |
Create a button, panel, label, checkbox, edit, spectrum, or listbox; returns a handle |
controls.align(handle [, value]) |
Get/set none, left, top, right, bottom, or client |
controls.parent(handle [, parentHandle]) |
Get/set the container; the getter returns 0 for the main window, and nil/0 moves the control there |
controls.width/height(handle [, value]) |
Get/set the preferred size |
controls.position/move(handle, x, y) |
Get/set the preferred position |
controls.text/caption(handle [, value]) |
Get/set a caption, label, or edit value |
controls.visible/show/hide(handle) |
Show or hide a control; hidden aligned controls release their space |
controls.hover/down/clicked(handle) |
Read mouse state |
controls.border(handle [, style]) |
0 none, 1 thin, 2 thick, 3 sizable |
controls.backcolor(handle [, color]) |
Set a color from the colors table |
BoundsRect stores a control's preferred position and size. Docking changes only
its effective WindowRect, so controls.width, controls.height, and
controls.position keep reporting the preferred geometry. Changing a child's
size realigns its siblings, and resizing the main window propagates through
every container level. See demos/controls_align.ls for a left-docked panel
with top- and bottom-docked buttons.
| Function | Returns | Description |
|---|---|---|
sleep(ms) |
— | Pause the script thread for ms milliseconds |
cycle |
true |
Block until the next drawing frame completes. Use while cycle do (instead of while true do) to run the loop body at most once per drawn frame, so drawing commands cannot pile up inside a single raylib drawing cycle |
frametime() |
number (seconds) |
Time elapsed since the last frame |
time() |
number (seconds) |
Elapsed time since the window was created |
rand(min, max) |
integer |
Random integer in [min, max] |
cycle is a frame gate: reading it pauses the script thread until the main
loop has presented the next drawing frame (EndDrawing). It never ends on its
own (while cycle do runs forever), and it only waits when a window is being
drawn — scripts running on the main thread (e.g. console lines) never block.
window.show()
while cycle do -- one iteration per drawing frame
canvas.rectangle(0, 0, canvas.width, canvas.height, true)
canvas.circle(320, 240, 20, true)
end -- no sleep() needed for ~60 FPS pacingFor sleep-based pacing you can still use the classic loop:
window.show()
start = time()
while true do
ft = frametime()
canvas.text(10, 10, "frame: " .. ft)
sleep(16) -- ~60 FPS pacing
end
print("uptime: " .. (time() - start))See demos/README.md for the full demo index. Run any demo with:
tyro demos/<name>.lua
| File | Feature |
|---|---|
demos/pong.lua |
Complete Pong game — drawing, keyboard input, AI, physics, collision, sound, scoring |
demos/basic_drawing.lua |
All drawing primitives: rectangle, circle, line, point, text, colors |
demos/animated_demo.lua |
Animation loop with random colors and sleep timing |
demos/cycle_demo.lua |
Per-frame loop using while cycle do — one drawing per frame |
demos/sprites_demo.lua |
Sprites system: load, show, hide, move, rotate, scale, named access |
demos/controls.ls |
Generic buttons, labels, checkboxes, edits, and panels |
demos/controls_align.ls |
Nested controls: a left-docked panel with top- and bottom-docked buttons |
demos/aseprites_demo.ls |
Aseprite animations: load .aseprite files (all frames as textures), play(fps), stop, looping, per-frame stepping — idle/walk/run showcase |
demos/interactive_paint.lua |
Mouse drawing with keyboard color switching (uses input APIs) |
demos/console_demo.lua |
Console output: print, println, log |
demos/terminal_demo.lua |
Built-in terminal commands: dir, list, clear, help, exit |
demos/console_read_demo.lua |
Interactive console.read() — prompt the user for input from Lua |
demos/music_demo.lua |
Sound effects (music.sound) and MML melodies (music.mml) |
demos/test.ls |
Circle animation with random colors |
demos/colors_bar.lua |
Full color palette display |
demos/multiply.lua |
Drawing + MML sound |
demos/text.lua |
Multi-language text rendering |
demos/shader_demo.lua |
Post-processing shaders: water, glow, gray, sepia, invert, vignette, pixelate |
demos/shake_demo.ls |
Screen shake: shake(ms, power) / window.shake(ms, power) — the world jolts like an accident or an error |
Script source runs on a worker thread, never on the render/main thread.
Every RayLib call and every control/console/audio mutation a script makes is
packaged as a queue object (TQueueObject) and dispatched back to the main
thread, where the main loop drains the queue (Main.Queue) once per cycle.
- Main-thread confinement. Window, canvas, sprites, shaders, fonts, music/
sound (the audio device), radio, spectrum and the control tree are all owned
by the main thread and must only be touched there. The Lua facades hide this:
they enqueue
TCreateControlObject/TSetControl*Object/draw/music objects that execute on the main thread. - Worker lifecycle.
Startpublishes an atomicstartedflag,Stop(also triggered by--exit/window close) cancels a pending queue wait, signals blockedconsole.read(), then pumpsCheckSynchronizeuntil the thread has actually finished beforeWaitForand disposal. - Queue admission. A stopped/inactive script rejects new asynchronous work
(the request is freed immediately instead). Right after the worker exits the
engine cancels and clears any stale queue entries so they cannot leak into a
later interactive run; under
--exita final drain runs first so work the worker accepted before completing is still honored. - Per-state Lua cancellation. Termination status lives in each Lua state's extra space, so cancelling one script can never affect another state.
- Resource release order. GPU/audio owners (sprites, canvases, shaders, fonts, musicians, generated waveforms, radio, spectrum) are destroyed while the window/OpenGL context and audio device are still alive; the context and device are closed last, after every owner has been released.
The worker thread reads current input/timing values (iskeydown, mousex,
frametime, ...) and submits drawing/mutation requests through the queue; it
must not hold references into the control tree or RayLib objects across
calls. Scripts blocking on console.read() or while cycle do are cancelled
by Stop, so a shut-down never waits on user input.
There is problem in raylib, in fact in OpenGL that cannot/not easy share texture between threads, we need another trick to pass drawing commands to main thread, but now i am sending objects to draw it in main thread, it is work fine until now
Use FreePascal 3.x or Lazarus with it
You need only MiniLib package minilib.lpk minilib
raylib for raylib.dll/so put it in same of tyro exe folder
Lua for lua dll 5.3 in same of tyro exe folder
You do not need to use it, it is already in the source folder
Thanks for https://pixelfonts.org/#116 https://github.com/IT-Studio-Rech/bdf-fonts https://forums.adafruit.com/viewtopic.php?t=203655