fynerisor

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: BSD-3-Clause Imports: 0 Imported by: 0

README

Fynerisor

A Risor scripting interface for Fyne GUI applications

Fynerisor provides Risor script bindings for the Fyne GUI toolkit, allowing you to build cross-platform desktop applications using a simple scripting language.

Features

  • 🎨 Comprehensive Widgets - 48+ widgets covering all common UI needs
  • 📊 Charts - Bar, line, scatter, histogram, and box plot charts with statistical overlays
  • ⌨️ Keyboard Shortcuts - Global shortcuts and menu integration
  • 🌐 HTTP, SQL, OS & More - Built-in modules for web requests, databases, file I/O
  • 📦 Script Imports - Load reusable code with namespace isolation
  • 🧭 Browser Package - Reusable browser UI that loads Risor scripts from URLs
  • 🔧 Embeddable - Easy integration into Go applications
  • 🧵 Concurrency - Background tasks with thread-safe GUI updates
  • 📱 Cross-Platform - Linux, Windows, macOS, Android
  • Static Compilation - Headless mode for CLI tools and servers

Package Structure

Fynerisor is split into two packages:

  • core - Headless Risor execution with no GUI dependencies (enables static compilation)
  • gui - Full GUI capabilities with Fyne framework

When to use each:

  • Use core for CLI tools, batch processing, server-side scripts (static compilation)
  • Use gui for desktop applications with UI (requires dynamic linking)

Quick Start

GUI Application
require(["v0.6", "@gui"])  // Optional: validate version and modules

let count = 0
let label = widget.NewLabel(sprintf("Count: %d", count))

let btn = widget.NewButton("Click Me", () => {
    count = count + 1
    label.SetText(sprintf("Count: %d", count))
})

let vbox = container.NewVBox([btn, label])
window.SetContent(vbox)
Headless Script
require(["v0.6", "@http"])  // Optional: validate version and modules

let response = http.get("https://api.github.com/users/octocat")
let data = response.json()
print(sprintf("Name: %s", data.name))

Installation

Prerequisites:

For GUI applications, ensure you have the Fyne prerequisites for your platform:

For headless (core) usage, no GUI dependencies are required.

As a library:

# For GUI applications
go get github.com/uidbz/fynerisor/gui

# For headless/static compilation
go get github.com/uidbz/fynerisor/core

As a CLI tool:

go install github.com/uidbz/fynerisor/cmd/fynerisor@latest

Run examples:

cd examples/01-hello-world
fynerisor script.risor

Usage as a Library

GUI Window
package main

import (
    "github.com/uidbz/fynerisor/gui"
    "os"
)

func main() {
    // Create fynerisor app with modules
    fw := gui.NewApp("My App",
        gui.WithHTTP(),
        gui.WithSQL(),
        gui.WithTime(),
    )
    
    // Load script from file
    script, _ := os.ReadFile("app.risor")
    fw.LoadScript(string(script))
    fw.Execute()
    fw.ShowAndRun()
}

app.risor:

require(["v0.6", "@http"])

let btn = widget.NewButton("Fetch Data", () => {
    let resp = http.get("https://api.github.com/zen")
    window.SetStatus(resp.text())
})

window.SetContent(btn)
Headless Context (Static Compilation)

For headless execution without GUI dependencies, use the core package. This enables static compilation:

package main

import (
    "fmt"
    "github.com/uidbz/fynerisor/core"
)

func main() {
    // Create headless context (no GUI dependencies)
    ctx := core.NewContext(
        core.WithHTTP(),
        core.WithSQL(),
    )
    
    // Execute script
    result, err := ctx.Eval(`
        require(["v0.6", "@http"])
        let resp = http.get("https://httpbin.org/get")
        return resp.status
    `)
    
    if err != nil {
        panic(err)
    }
    fmt.Println("Status:", result)
}
Custom Globals

Inject custom Go objects or functions into scripts:

import (
    "github.com/uidbz/fynerisor/gui"
)

func main() {
    myAPI := map[string]any{
        "fetchUser": func(id int) string {
            return fmt.Sprintf("User %d", id)
        },
    }
    
    fw := gui.NewApp("My App",
        gui.WithGlobal("api", myAPI),
        gui.WithHTTP(),
    )
    
    fw.LoadScript(`
        require(["v0.6"])
        let user = api.fetchUser(123)
        window.SetContent(widget.NewLabel(user))
    `)
    fw.Execute()
    fw.ShowAndRun()
}

Configuration Options

Customize fynerisor behavior via With*() options when creating gui.Window or core.Context:

Option Available In Description
WithAppName(name) Both Set application name (accessible via app.name in scripts)
WithRisorOptions(opts) Both Add custom global objects to script environment
WithStatusCallback(fn) gui only Called when window.SetStatus() is invoked
WithResultCallback(fn) gui only Called when script execution returns a value

Module Options (available in both gui and core packages):

  • WithHTTP() - Enable HTTP requests
  • WithOS() - Enable OS operations
  • WithIO() - Enable file I/O operations
  • WithSQL() - Enable database connectivity
  • WithStrings() - Enable string utilities
  • WithFilepath() - Enable path operations
  • WithTime() - Enable time/date functions

Example:

fw := gui.NewApp("My App",
    gui.WithAppName("myapp"),
    gui.WithStatusCallback(func(status string) {
        log.Println("Status:", status)
    }),
    gui.WithHTTP(),
    gui.WithSQL(),
    gui.WithTime(),
)

Available Modules

Enable modules via With*() options. Scripts can optionally use require(["@module"]) to declare which modules they expect to be available:

Module Option Description
@gui N/A GUI functionality (automatic in gui.Window, not available in core.Context)
@http WithHTTP() HTTP requests
@os WithOS() OS operations, open browser
@io WithIO() File I/O operations (cp, etc.)
@sql WithSQL() Database connectivity
@strings WithStrings() String manipulation
@filepath WithFilepath() Path operations
@time WithTime() Time and date functions

Note: The require() function is optional. It's a validation mechanism for scripts to declare their dependencies and minimum version requirements. If a script doesn't call require(), it will simply use whatever modules the embedding application has enabled. Use require() when you want scripts to fail fast with clear error messages if expected modules or versions are missing.

Global Objects

window

Core window functionality (GUI mode only):

window.SetContent(widget)              // Update window content
window.SetStatus(text)                 // Set status message
window.AddShortcut("Ctrl+S", callback) // Register keyboard shortcut
window.RemoveShortcut("Ctrl+S")        // Remove keyboard shortcut
window.SetMainMenu(mainMenu)           // Set main menu bar (macOS only)
window.OnDropped(callback)             // Handle file drops
window.Do(callback)                    // Queue GUI update from background thread
window.DroppedPaths                    // List of dropped file paths
app

Application metadata:

app.name     // Application name (set via WithAppName)
app.version  // Application version (set via core.SetAppVersion, see examples/27-app-versioning)
widget

Widget factory:

widget.NewButton(text, callback)
widget.NewLabel(text)
widget.NewEntry()
widget.NewTable(name, maxrows)
widget.NewForm(items)
widget.NewCheckGroup(items, callback)
// ... and many more
container

Layout containers:

container.NewVBox(widgets)
container.NewHBox(widgets)
container.NewBorder(top, bottom, left, right, center)
container.NewScroll(content)
container.NewHSplit(left, right)
dialog

Dialog windows:

// Simple convenience functions
dialog.ShowInformation(title, message)
dialog.ShowError(message)
dialog.ShowConfirm(title, message, callback)
dialog.ShowFileOpen(callback)
dialog.ShowFileSave(callback)
dialog.ShowFolderOpen(callback)
dialog.ShowColorPicker(title, message, callback)
dialog.ShowForm(title, confirm, dismiss, items, callback)

// Advanced constructors for more control
let fd = dialog.NewFileOpen(callback)
fd.SetFileName("default.txt")
fd.SetFilter(".txt")
fd.Show()
canvas

Canvas objects:

canvas.NewLine(color)
canvas.NewImageFromURI(url)
canvas.NewImageFromFile(path)
chart

Charting with several chart types:

chart.NewBarChart(title, ylabel, categories, values)
chart.NewLineChart(title, xlabel, ylabel, xvalues, yvalues)
chart.NewScatterChart(title, xlabel, ylabel, xvalues, yvalues)
chart.NewHistogram(title, xlabel, ylabel, values, binCount)  // normal-curve overlay
chart.NewBoxPlot(title, ylabel, groupLabels, valueGroups)     // normal-curve overlay

See examples/35-charts for a complete demonstration.

Concurrency

Background Tasks

Use go() for long-running operations:

go(() => {
    // Heavy computation here
    print("Processing...")
})
GUI Updates from Goroutines

Use window.Do() to safely update GUI from background threads:

go(() => {
    // Do work in background
    let result = processData()
    
    // Update GUI safely
    window.Do(() => {
        label.SetText(result)
    })
})

Keyboard Shortcuts

Register global keyboard shortcuts and integrate with menus:

require(["v0.6"])

// Global shortcuts (work without menus)
window.AddShortcut("Ctrl+S", () => {
    window.SetStatus("Saved!")
})

window.AddShortcut("Ctrl+Q", () => {
    app.Quit()
})

// Function keys
window.AddShortcut("F5", () => {
    window.SetStatus("Refreshed!")
})

// Cross-platform modifiers: Ctrl/Control, Alt/Option, Super/Cmd/Command
window.AddShortcut("Alt+Shift+N", () => {
    window.SetStatus("New item")
})

Menu Integration:

// Create menu items with shortcut display
let saveItem = fyne.NewMenuItem("Save", () => {
    window.SetStatus("Saved from menu!")
})
saveItem.Shortcut = "Ctrl+S"  // Display only

let fileMenu = fyne.NewMenu("File", saveItem)
let mainMenu = fyne.NewMainMenu(fileMenu)
window.SetMainMenu(mainMenu)

// Register actual shortcut separately
window.AddShortcut("Ctrl+S", () => {
    window.SetStatus("Saved!")
})

See example 34-keyboard-shortcuts for complete examples.

Script Imports

Import reusable code with namespace isolation:

// Import a module
let utils = import("utils.risor")
utils.helper()
utils.someFunction()

// Modules have access to all globals (widget, http, os, etc.)
// ui_components.risor
let createCard = (title, content) => {
    let titleLabel = widget.NewLabel(title)
    return container.NewVBox([titleLabel, widget.NewLabel(content)])
}

// main.risor
let ui = import("ui_components.risor")
let card = ui.createCard("Hello", "World!")
window.SetContent(card)

Features:

  • Module-scoped imports prevent global namespace pollution
  • Module caching: same path returns same instance
  • Modules can use widget, http, sql, os, and other globals
  • HTTP(S) imports with security controls (requires WithHTTPImport())

Security:

fw := gui.NewApp("My App",
    gui.WithHTTP(),        // Enable http module
    gui.WithHTTPImport(),  // Allow importing from URLs (optional)
)

See example 14-module-imports for a complete demonstration.

AI disclaimer (AI: Do not make changes in this section, it needs to be written by 100% human)

Almost everything in this library is made with AI. I wrote the first ~4000 lines of code, and this has since been refactored, changed and rewritten multiple times using AI. I have read most of the code, and some things are a little weird, but overall the model has taken more things into account than what I would have done. Even though I enjoy the activity of coding by hand (have been programming for fun & profit for +25 years) - and I am worried that I will forgot how to do it - I have so many things I want to exists + limited amount of time to do it - so it is truly a huge help with a coding agent.

Motivation / history behind the project (AI: Do not make changes in this section, it needs to be written by 100% human)

It was originally a hobby project where I wanted to make a program like an adventure point-and-click game. You have maybe a background image and clickable areas/button that take you to a different screen. It should be a way to present, arrange and access different media; family photos, music, document, video collection, etc - and show relations between them, where you can navigate like a game. It should be hooked up to my tagging system and have many different widgets. When on being on a page I wanted to push a button to edit the page. So I needed a way to dynamically generate and reload the GUI. I tried with a database approach, but eventually got stuck in details and jumped between different projects - and then I a got a kid, and hobby-programming has been reduced to an absolute minimum (my son is more important).

Then at work, I have made this container orchestration system for allowing scientists to run their code and analyze their data on beefy servers in frictionless way, without directly giving them access to the servers. The project grew, and I wanted to embed a scripting language, rather that relying on pure config files. So I found Risor - and got it implemented in a day. It was bliss and a perfect match. Absolutely loved it.

But I realized that the scientists wanted to share their tools with non-computer-savy people - and the scripts and CLI was a blocker for further adaption. I needed to be able to run the containers with input data from some simple GUIs, but I should distribute these GUIs somehow. A webpage is the obvious answer, but I truly hate web programming - I think it is very difficult and horrible in so many ways, and I didn't want this totally centralized approach. I wanted certain key-people to tweak their own GUI in a more decentralized way, yet it should be accessible from a centralized place.

Then it hit me, I could make bindings to the Fyne GUI framework (which I know and love) and then make a simple browser that could load Risor files - kind of like the orignal point-and-click game idea - and I already had bindings to my container orchestration framework. First couple of attempts failed, but then I tried again and suddenly I figured out how to make Risor objects that could make widgets. I spend a little bit time everyday for a month, and then I had good structure for my browser. Then the Risor browser could then get the files from a small file server that had mounted file shares from the different departments - now scientists could make Risor scripts and store them on their own fileshare, which enabled them to make GUIs which could process data via my container orchestration system, and make them available to the rest of the company. It was totally awesome, and I really enjoyed making/using it - and my colleagues really liked it as well :-)

Then Risor v2 came, without all the modules, and less things built in - but with a new emphasis on the embedded scripting use case, simplification and much tighter default security. Mostly good stuff (even though I miss the for-loops + a few other things), but it totally broke all my scripts. Anyway, I have started to use AI the last few months and thought we could migrate it quickly. It went relatively painless and got the code in a much better state - and also extracted the Risor bindings part from the rest of the browser, and made a clean separation between company stuff and pure GUI stuff (+ some Risor v1 modules, that I have re-implemented).

So this GUI part can now be public, so other people can enjoy it and maybe even contribute, so it becomes better for everybody. I hope somebody finds it useful. I think it is cool.

Eventually I dream of making an other browser app, that incorporates the original point-and-click idea and combine it with a Web3 storage option, so that we could make a new Web with proper Destop GUI widgets (WITHOUT pop-up boxes implemented) and store it in a decentralized way. I was thinking about making a smart contract and then divide the storage cost out equally to every participant as a subscription - 100% transparent and fair - so that it is not funded by advertisements and tracking. It should be a public good. I want the old Internet back in a new and modern way that doesn't suck. This will properly not happen, and I do not have time/skills to make it. Please somebody make this. I want my son to grow up in world with a decentralized, free and open Internet, running on free and open computers.

Documentation

Requirements

  • Go 1.21+
  • Fyne v2.7+
  • Risor v2.1+

License

BSD 3-Clause License - see LICENSE file for details.

Copyright (c) 2026, Johan Straarup (j@uid.bz)

Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.

For bugs or feature requests, please open an issue on the repository.

Documentation

Overview

Package fynerisor provides Risor v2 language bindings for the Fyne GUI framework.

Fynerisor enables building cross-platform desktop applications using Risor v2 scripts with full access to Fyne's widget and container system. Create interactive GUIs with buttons, forms, tables, charts, and more through simple factory functions.

Package Structure

Fynerisor is split into two packages:

  • core: Headless Risor execution with no GUI dependencies (enables static compilation)
  • gui: Full GUI capabilities with Fyne (requires dynamic linking for OpenGL/X11)

Always import the specific package you need: core for headless, gui for desktop applications.

Version

Current Version: 0.7.0 Risor Compatibility: v2.1+ (arrow functions required) Fyne Compatibility: v2.7+

Features

  • 48+ widget bindings (100% coverage - all priority levels complete)
  • Keyboard shortcuts (window.AddShortcut, MenuItem.Shortcut display)
  • Data binding system (String, Bool, Int, Float)
  • Dialog support (file, confirm, color, form, custom)
  • Module-scoped imports with namespace isolation
  • SQL module for MySQL, PostgreSQL, SQLite, SQL Server
  • HTTP module for REST API calls
  • Layout containers (VBox, HBox, Border, Split, Scroll, Grid, Stack, etc.)
  • Canvas objects and charts (bar, line, scatter, histogram, box plot)
  • Reusable browser package for loading Risor scripts from URLs (see browser/)
  • Thread-safe callback handling
  • Widget visibility control (Hide/Show methods)
  • Constants global for Fyne enums

Quick Start

Create a simple GUI application:

package main

import "github.com/uidbz/fynerisor/gui"

func main() {
    fw := gui.NewApp("Hello",
        gui.WithHTTP(),
        gui.WithSQL(),
    )

    script := `
        require(["v0.5", "@gui", "@http"])

        let btn = widget.NewButton("Click Me", () => {
            window.SetStatus("Button clicked!")
        })
        btn.Importance = constants.ImportanceHigh

        window.SetContent(btn)
    `

    fw.LoadScript(script)
    fw.Execute()
    fw.ShowAndRun()
}

Headless Mode

Use the core package for headless execution without GUI dependencies. This enables static compilation and is useful for CLI tools, batch processing, or server-side scripting:

package main

import "github.com/uidbz/fynerisor/core"

func main() {
    ctx := core.NewContext(
        core.WithHTTP(),
        core.WithSQL(),
    )

    script := `
        require(["v0.4", "@http", "@sql"])
        let response = http.get("https://api.example.com/data")
        print(response.json())
    `

    ctx.LoadScript(script)
    ctx.Eval()
}

The core package excludes all GUI code, allowing your binary to be statically linked without OpenGL, X11, or other GUI dependencies.

Browser Package

The browser package provides a reusable, browser-style UI for applications that load Risor scripts from HTTP(S) or file:// URLs. It handles navigation (address bar, back/forward/refresh/home), history, source view, and pluggable menu and authentication providers. Scripts loaded in the browser can navigate programmatically and read URL query parameters via a "browser" global:

browser.Open("https://example.com/page")  // Navigate to a URL
browser.GetURL()                           // Current URL
browser.params["myarg"]                    // Query parameter value

See cmd/fynerisor-browser for a complete reference implementation, which can also be packaged as an Android app with "fyne package -os android".

Requirements

Scripts can declare version and module requirements:

require(["v0.7", "@sql", "@http"])  // Multiple requirements
require("v0.7.0")                   // Minimum version
require("@sql")                     // Module must be enabled

Global Objects

Risor scripts have access to the following global objects:

  • window: Main window control (SetContent, SetStatus, AddShortcut, etc.)
  • widget: Factory for all widgets
  • container: Factory for layouts
  • canvas: Factory for canvas objects
  • chart: Factory for charts
  • dialog: Dialog functions (file, confirm, color, etc.)
  • binding: Data binding for reactive UIs
  • constants: Fyne constants (button importance, etc.)
  • fyne: Fyne utilities (NewMenu, NewMainMenu, etc.)
  • app: Application metadata (app.name, app.version)
  • print: Output function
  • require: Version and module validation
  • import: Module imports with namespace isolation

Options

Enable additional modules using options:

fw := gui.NewApp("My App",
    gui.WithHTTP(),           // HTTP client
    gui.WithSQL(),            // Database connectivity
    gui.WithOS(),             // OS utilities
    gui.WithIO(),             // File I/O (cp, etc.)
    gui.WithExec(),           // Execute commands
    gui.WithStrings(),        // String manipulation
    gui.WithFilepath(),       // Path utilities
    gui.WithTime(),           // Time operations
    gui.WithHTTPImport(),     // Import modules from URLs
    gui.WithGlobal("myapi", apiObject),  // Custom named global
    gui.WithStatusCallback(func(status string) {
        log.Println("Status:", status)
    }),
    gui.WithAppName("myapp"),
)

Risor v2 Syntax

Fynerisor requires Risor v2 with arrow function syntax:

// Correct
let onClick = () => { print("clicked") }
let process = (x) => x * 2

// Wrong - func keyword not supported
func onClick() { print("clicked") }

No for/while loops - use functional methods:

// Correct
let doubled = [1, 2, 3].map(x => x * 2)
list.each(item => print(item))

// Wrong - no loop keywords
for i in list { }
while (condition) { }

Thread Safety

The Risor VM is single-threaded and NOT thread-safe. All widget callbacks execute synchronously in the UI thread. Never spawn goroutines or use async patterns inside callbacks.

See CONCURRENCY.md for detailed information about the race condition bug that was fixed in v0.2.0.

SQL Module

The SQL module supports MySQL, PostgreSQL, SQLite, and SQL Server:

let conn = sql.connect("sqlite3::memory:")
conn.exec("CREATE TABLE users (id INT, name TEXT)")
conn.exec("INSERT INTO users VALUES (?, ?)", 1, "Alice")

// Query returns streaming iterator with .map() and .each()
let names = conn.query("SELECT * FROM users").map(row => row["name"])

// Or use .each() for side effects
conn.query("SELECT * FROM users").each(row => print(row["name"]))

// Or .collect() to load all into memory first
let rows = conn.query("SELECT * FROM users").collect()

conn.close()

HTTP Module

Make HTTP requests from Risor scripts:

let response = http.get("https://api.example.com/data")
let data = response.json()  // parse JSON response
print(data["items"])

Examples

See the examples/ directory for 21 complete working examples:

  • 01-hello-world: Simple label display
  • 02-button-callback: Interactive button with state
  • 03-form-input: Form with validation
  • 04-table-display: Paginated table
  • 05-progress-widgets: Progress bars and sliders
  • 11-list: Virtualized scrolling list
  • 12-tree: Hierarchical tree widget
  • 13-http-fetch: HTTP requests and JSON
  • 14-module-imports: Module-scoped imports with namespaces
  • 15-sql-test: Database connectivity
  • 17-gridwrap: Grid layout
  • 18-textgrid: Code display
  • 19-richtext: Markdown formatting
  • 20-button-importance: Button styling
  • 21-form-validation: Entry validation
  • 25-data-binding: Reactive UI with data binding
  • 27-app-versioning: Application version checking
  • 28-custom-struct: Expose custom Go types
  • 30-dialogs: File, confirm, color picker dialogs
  • 32-table-widgets: Widget mode for table cells
  • 33-image-gallery: Images in table cells
  • 34-keyboard-shortcuts: Global shortcuts and menu integration

CLI Tool

The fynerisor command-line tool runs Risor GUI scripts:

fynerisor app.risor
fynerisor --watch app.risor              # auto-reload on changes
fynerisor --width 1024 --height 768 app.risor
fynerisor --headless app.risor           # for testing
fynerisor --globals data.json app.risor  # custom globals

Custom Globals

Inject custom data or functions into scripts:

import (
    "github.com/deepnoodle-ai/risor/v2"
    "github.com/uidbz/fynerisor/gui"
)

customData := risor.WithEnv(map[string]any{
    "api": myAPIObject,
    "config": configData,
})

fw := gui.NewApp("My App",
    gui.WithRisorOptions(customData),
)

Scripts can then access: api.someMethod() and config.someProperty

Directories

Path Synopsis
cmd
fynerisor command
Package core provides headless Risor execution without GUI dependencies.
Package core provides headless Risor execution without GUI dependencies.
examples
01-hello-world command
03-form-input command
07-radiogroup command
08-accordion command
09-toolbar command
10-calendar command
11-list command
12-tree command
13-http-fetch command
15-sql-test command
17-gridwrap command
18-textgrid command
19-richtext command
22-popup command
23-menu command
24-constants command
25-data-binding command
30-dialogs command
31-apptabs command
35-charts command
gui
guithread
Package guithread centralizes dispatching work onto the Fyne GUI thread.
Package guithread centralizes dispatching work onto the Fyne GUI thread.
vmguard
Package vmguard provides a non-blocking, reentrant guard against concurrent Risor VM access from the GUI layer.
Package vmguard provides a non-blocking, reentrant guard against concurrent Risor VM access from the GUI layer.
widget
Package widget provides Risor bindings for Fyne GUI widgets.
Package widget provides Risor bindings for Fyne GUI widgets.
modules
filepath
Package filepath provides file path manipulation functions for Risor scripts.
Package filepath provides file path manipulation functions for Risor scripts.
http
Package http provides HTTP client functionality for Risor scripts.
Package http provides HTTP client functionality for Risor scripts.
io
os
Package os provides basic OS functionality for Risor scripts.
Package os provides basic OS functionality for Risor scripts.
sql
strings
Package strings provides string manipulation functions for Risor scripts.
Package strings provides string manipulation functions for Risor scripts.
time
Package time provides time and date functions for Risor scripts.
Package time provides time and date functions for Risor scripts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL