Guides
The frontend
How pages load during development and in builds, the mygo-runtime package, custom protocols, custom title bars and dropped files.
An app's user interface is a web frontend: HTML, CSS and JavaScript built with any tools. New projects use TypeScript and Vite, but MyGo only needs a URL to load, during development, and a directory of built files, for builds.
How pages load
Windows load pages of the frontend with URLs without a scheme, such as /
or /settings?tab=general:
mygo.NewWindow(mygo.WindowOptions{URL: "/"})
win.LoadURL("/settings")They resolve against:
- during
mygo dev, the dev server atdevUrlin mygo.config.ts, whichdevCommandstarts, e.g.http://localhost:5173/, so the dev server's hot reload works in the app; - in builds,
mygo://localhost/, which serves the files offrontendDistthatmygo buildembeds into the executable after runningbuildCommand.
export default defineConfig({
devUrl: "http://localhost:5173",
devCommand: "bun run dev:web",
buildCommand: "bun run build:web",
frontendDist: "dist",
});Without devUrl, mygo dev serves frontendDist from disk instead. Paths
without a file extension that match no file serve index.html, so
client-side routers work.
The frontend is a secure context, and its origin is mygo://localhost on
macOS and Linux and http://mygo.localhost on Windows, where WebView2
serves custom schemes under http://<scheme>.localhost. Storage such as
localStorage and IndexedDB belongs to an origin, so data a page stores
under mygo dev (the dev server's origin) is not what the built app sees.
Keep data you care about in Go, for example in a file in
App.Path(mygo.PathUserData), and hand it to pages through bound methods.
Windows can also load other content:
win.LoadURL("https://example.com"), a web page;win.LoadHTML(html, baseURL), an HTML string;win.LoadFile("docs/index.html"), a local file, resolved against the working directory, then the executable's directory (and the Resources directory of a macOS app).
Pages that are not the app's own cannot call Go methods: see who may call.
Without the CLI
mygo build embeds the frontend by calling mygo.SetFrontend, and a
program built with go build can do the same with embed:
//go:embed all:dist
var dist embed.FS
func main() {
web, _ := fs.Sub(dist, "dist")
mygo.SetFrontend(web)
// ...
}Custom protocols
mygo.Protocol.Handle serves a URL scheme from an http.Handler, for
content your app produces or reads at run time: pages, images, files of
the user. Handlers run on their own goroutines and stream their responses:
writes wait while the webview is behind, so a large file is never held in
memory whole. Pages load such URLs like web URLs, with fetch, <img>,
ES modules and relative URLs:
// Thumbnails generated into the cache directory.
cache, _ := mygo.App.Path(mygo.PathCache)
mygo.Protocol.Handle("thumbs", mygo.FileServer(os.DirFS(cache)))
// Anything an http.Handler does.
mygo.Protocol.HandleFunc("api", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"path": r.URL.Path})
})<img src="thumbs://localhost/photo-42.png" />Register schemes before creating the windows that use them.
mygo.FileServer serves an fs.FS with content types and range requests,
falling back to index.html like the frontend. Handling the mygo scheme
replaces the frontend with your handler. Pages of custom schemes are the
app's own: they may call Go methods.
The mygo-runtime package
MyGo injects its runtime into every page as window.mygo. The
mygo-runtime npm package, which new projects depend on, gives it types
and imports; the generated client is built on it.
import { currentWindow, isMyGo, onFileDrop, runtime } from "mygo-runtime";
if (isMyGo() && runtime().platform === "darwin") {
document.documentElement.classList.add("mac");
}
await currentWindow.toggleMaximize();| Export | What it does |
|---|---|
call(method, ...args), on(event, listener), once(event, listener), event<T>(name) |
untyped calls and events, see bindings |
Channel |
a stream of values from a Go method, see channels |
isCallError(err) |
whether a rejection came from a Go error |
isMyGo() |
whether the page runs in a MyGo window, not in a browser tab of the dev server |
runtime() |
the runtime: platform ("darwin", "linux" or "win32"), windowId (the Go window's ID()) and version; throws outside MyGo |
currentWindow |
the page's window: minimize, maximize, unmaximize, toggleMaximize, isMaximized, toggleFullScreen, close and setTitle |
onFileDrop(listener) |
files dropped on the window, with their paths |
Opened in a regular browser, which is handy for working on the layout,
pages have no runtime: calls reject and isMyGo() is false.
Custom title bars
A frameless window has no title bar or borders, and the page draws its own.
Mark the elements that drag the window with the CSS property
--app-region: drag:
mygo.NewWindow(mygo.WindowOptions{URL: "/", Frameless: true}).titlebar {
--app-region: drag;
height: 40px;
}
.titlebar .search {
--app-region: no-drag;
}import { currentWindow } from "mygo-runtime";
minimizeButton.onclick = () => currentWindow.minimize();
maximizeButton.onclick = () => currentWindow.toggleMaximize();
closeButton.onclick = () => currentWindow.close();The property is inherited: no-drag excludes parts of a region again, and
buttons, links, inputs and other interactive elements inside a region stay
clickable. Double-clicking a region does what double-clicking a title bar
does.
Frameless windows still resize from their edges. On Linux, where GTK gives windows without decorations no borders, the outermost 5 px of the page act as the borders: they show resize cursors, and the page gets no clicks or pointer moves there.
On Linux a frameless window also keeps the menu bar that App.SetMenu or
Window.SetMenu gives it, above the page and so above a custom title bar.
Give such a window an empty menu of its own,
win.SetMenu(mygo.NewMenu(nil)), and show menus from the page instead
(Menu.PopupAt, from a bound method the page calls).
Hidden title bars
A window can keep its window controls and only hide the title bar, so the
page extends under them and draws the rest of the title bar: set
TitleBarStyle to mygo.TitleBarHidden. The controls stay at the top of
the window:
- On macOS, the traffic lights.
mygo.TitleBarHiddenInsetleaves more room around them, andTrafficLightPositionmoves them: the position of the close button's top-left corner in the window. - On Windows, minimize, maximize or restore, and close at the top-right
corner, drawn as Windows 11 draws its own. They fill the height of the
title bar,
TitleBarHeight(32 by default), and hovering maximize opens the snap layouts. The window resizes from its top edge. - On Linux, GTK's own title buttons, where the desktop's button layout puts
them, centered in
TitleBarHeight(by default the height of the desktop's header bars). The layout decides which buttons show, possibly none, as tiling window managers are often set up. The outer pixels of the page resize the window, as a frameless one's.
mygo.NewWindow(mygo.WindowOptions{
URL: "/",
TitleBarStyle: mygo.TitleBarHidden,
TitleBarHeight: 40,
})From its first paint, the page learns the room the controls take from
CSS variables on :root:
| variable | |
|---|---|
--mygo-titlebar-height |
the height of the title bar |
--mygo-titlebar-inset-left, --mygo-titlebar-inset-right |
the room the controls take from the left and from the right edge |
They are 0px in full screen, where the controls hide (on macOS they come
back with the menu bar). The page's title bar keeps clear of the controls
and, marked with --app-region: drag, moves the window:
.titlebar {
--app-region: drag;
height: var(--mygo-titlebar-height, 40px);
padding-left: var(--mygo-titlebar-inset-left, 0px);
padding-right: var(--mygo-titlebar-inset-right, 0px);
}Transparent: true lets a page with a transparent background show the
desktop through, and Vibrancy puts a blurred material behind it on macOS
and Windows 11:
mygo.NewWindow(mygo.WindowOptions{
URL: "/",
TitleBarStyle: mygo.TitleBarHidden,
Transparent: true,
Vibrancy: mygo.VibrancySidebar, // Mica on Windows 11
})See examples/frameless for a complete custom title bar, and
examples/vibrancy for a translucent sidebar under an inset title bar.
Dropped files
Files dragged from Finder or Explorer and dropped on the page reach the page with their paths, which DOM drop events do not tell:
import { onFileDrop } from "mygo-runtime";
onFileDrop(({ paths, x, y }) => {
const target = document.elementFromPoint(x, y);
// ...
});and Go, with win.OnFileDrop. The page's own drag and drop keeps working:
its drop listeners still get the File objects, and dropping files where
the page does not handle them no longer replaces the page with the file.
Dark mode
Pages follow the system appearance through the prefers-color-scheme media
query. Declare color-scheme: light dark in CSS so that form controls and
scrollbars follow too, and give windows a BackgroundColor matching the
page in both appearances, which the window shows until the page paints:
:root {
color-scheme: light dark;
}mygo.NewWindow(mygo.WindowOptions{URL: "/", BackgroundColor: "light-dark(#f5f5f7, #1e1e1e)"})mygo.Theme.SetSource(mygo.ThemeDark) forces an appearance, which pages see
through the same media query.
Links and new windows
Links with target="_blank" and window.open() call the window's open
handler. Without one, http(s) URLs open in the default browser and
anything else is denied; win.SetWindowOpenHandler decides otherwise:
win.SetWindowOpenHandler(func(req mygo.WindowOpenRequest) *mygo.WindowOptions {
if strings.HasPrefix(req.URL, "https://docs.example.com/") {
return &mygo.WindowOptions{Width: 900, Height: 700} // a window of the app
}
go mygo.Shell.OpenExternal(req.URL) // the default browser
return nil
})win.OnWillNavigate can cancel the page's own navigations, e.g. to keep the
app's window on the app: see Windows.
Preload scripts
WindowOptions.PreloadScript runs JavaScript in every page of the window
before the page's own scripts, once window.mygo exists.
The web inspector
Development builds have the web inspector: right-click a page and choose
Inspect Element (Inspect on Windows), or call win.OpenDevTools().
Production builds do not, unless built with mygo build -debug or a window
sets DevTools: mygo.DevToolsEnabled.