Thanks to go-astilectron build cross platform GUI apps with GO and HTML/JS/CSS. It is the official GO bindings of astilectron and is powered by Electron.
Demo
To see a minimal Astilectron app, checkout out the demo.
For convenience purposes, a bootstrap has been implemented.
The bootstrap allows you to quickly create a one-window application.
There's no obligation to use it, but it's strongly recommended.
If you decide to use it, read thoroughly the documentation as you'll have to structure your project in a specific way.
Bundler
Still for convenience purposes, a bundler has been implemented.
The bundler allows you to bundle your app for every os/arch combinations and get a nice set of files to send your users.
Quick start
WARNING: the code below doesn't handle errors for readibility purposes. However you SHOULD!
Import go-astilectron
To import go-astilectron run:
$ go get -u github.com/asticode/go-astilectron
Start go-astilectron
// Initialize astilectronvara, _=astilectron.New(log.New(os.Stderr, "", 0), astilectron.Options{
AppName: "<your app name>",
AppIconDefaultPath: "<your .png icon>", // If path is relative, it must be relative to the data directoryAppIconDarwinPath: "<your .icns icon>", // Same hereBaseDirectoryPath: "<where you want the provisioner to install the dependencies>",
VersionAstilectron: "<version of Astilectron to utilize such as `0.33.0`>",
VersionElectron: "<version of Electron to utilize such as `4.0.1` | `6.1.2`>",
})
defera.Close()
// Start astilectrona.Start()
// Blocking patterna.Wait()
For everything to work properly we need to fetch 2 dependencies : astilectron and Electron. .Start() takes care of it by downloading the sources and setting them up properly.
In case you want to embed the sources in the binary to keep a unique binary you can use the NewDisembedderProvisioner function to get the proper Provisioner and attach it to go-astilectron with .SetProvisioner(p Provisioner). Or you can use the bootstrap and the bundler. Check out the demo to see how to use them.
Beware when trying to add your own app icon as you'll need 2 icons : one compatible with MacOSX (.icns) and one compatible with the rest (.png for instance).
If no BaseDirectoryPath is provided, it defaults to the executable's directory path.
The majority of methods are asynchronous which means that when executing them go-astilectron will block until it receives a specific Electron event or until the overall context is cancelled. This is the case of .Start() which will block until it receives the app.event.readyastilectron event or until the overall context is cancelled.
HTML paths
NB! All paths in HTML (and Javascript) must be relative, otherwise the files will not be found.
To make this happen in React for example, just set the homepage property of your package.json to "./".
{ "homepage": "./" }
Create a window
// Create a new windowvarw, _=a.NewWindow("http://127.0.0.1:4000", &astilectron.WindowOptions{
Center: astikit.BoolPtr(true),
Height: astikit.IntPtr(600),
Width: astikit.IntPtr(600),
})
w.Create()
When creating a window you need to indicate a URL as well as options such as position, size, etc.
This is pretty straightforward except the astilectron.Ptr* methods so let me explain: GO doesn't do optional fields when json encoding unless you use pointers whereas Electron does handle optional fields. Therefore I added helper methods to convert int, bool and string into pointers and used pointers in structs sent to Electron.
Open the dev tools
When developing in JS, it's very convenient to debug your code using the browser window's dev tools:
// Open dev toolsw.OpenDevTools()
// Close dev toolsw.CloseDevTools()
Add listeners
// Add a listener on Astilectrona.On(astilectron.EventNameAppCrash, func(e astilectron.Event) (deleteListenerbool) {
log.Println("App has crashed")
return
})
// Add a listener on the windoww.On(astilectron.EventNameWindowEventResize, func(e astilectron.Event) (deleteListenerbool) {
log.Println("Window resized")
return
})
Nothing much to say here either except that you can add listeners to Astilectron as well.
Play with the window
// Play with the windoww.Resize(200, 200)
time.Sleep(time.Second)
w.Maximize()
Check out the Window doc for a list of all exported methods
Send messages from GO to Javascript
Javascript
// This will wait for the astilectron namespace to be readydocument.addEventListener('astilectron-ready',function(){// This will listen to messages sent by GOastilectron.onMessage(function(message){// Process messageif(message==="hello"){return"world";}});})
GO
// This will send a message and execute a callback// Callbacks are optionalw.SendMessage("hello", func(m*astilectron.EventMessage) {
// Unmarshalvarsstringm.Unmarshal(&s)
// Process messagelog.Printf("received %s\n", s)
})
This will print received world in the GO output
Send messages from Javascript to GO
GO
// This will listen to messages sent by Javascriptw.OnMessage(func(m*astilectron.EventMessage) interface{} {
// Unmarshalvarsstringm.Unmarshal(&s)
// Process messageifs=="hello" {
return"world"
}
returnnil
})
Javascript
// This will wait for the astilectron namespace to be readydocument.addEventListener('astilectron-ready',function(){// This will send a message to GOastilectron.sendMessage("hello",function(message){console.log("received "+message)});})
This will print "received world" in the Javascript output
// If several displays, move the window to the second displayvardisplays=a.Displays()
iflen(displays) >1 {
time.Sleep(time.Second)
w.MoveInDisplay(displays[1], 50, 50)
}
Menus
// Init a new app menu// You can do the same thing with a windowvarm=a.NewMenu([]*astilectron.MenuItemOptions{
{
Label: astikit.StrPtr("Separator"),
SubMenu: []*astilectron.MenuItemOptions{
{Label: astikit.StrPtr("Normal 1")},
{
Label: astikit.StrPtr("Normal 2"),
OnClick: func(e astilectron.Event) (deleteListenerbool) {
log.Println("Normal 2 item has been clicked")
return
},
},
{Type: astilectron.MenuItemTypeSeparator},
{Label: astikit.StrPtr("Normal 3")},
},
},
{
Label: astikit.StrPtr("Checkbox"),
SubMenu: []*astilectron.MenuItemOptions{
{Checked: astikit.BoolPtr(true), Label: astikit.StrPtr("Checkbox 1"), Type: astilectron.MenuItemTypeCheckbox},
{Label: astikit.StrPtr("Checkbox 2"), Type: astilectron.MenuItemTypeCheckbox},
{Label: astikit.StrPtr("Checkbox 3"), Type: astilectron.MenuItemTypeCheckbox},
},
},
{
Label: astikit.StrPtr("Radio"),
SubMenu: []*astilectron.MenuItemOptions{
{Checked: astikit.BoolPtr(true), Label: astikit.StrPtr("Radio 1"), Type: astilectron.MenuItemTypeRadio},
{Label: astikit.StrPtr("Radio 2"), Type: astilectron.MenuItemTypeRadio},
{Label: astikit.StrPtr("Radio 3"), Type: astilectron.MenuItemTypeRadio},
},
},
{
Label: astikit.StrPtr("Roles"),
SubMenu: []*astilectron.MenuItemOptions{
{Label: astikit.StrPtr("Minimize"), Role: astilectron.MenuItemRoleMinimize},
{Label: astikit.StrPtr("Close"), Role: astilectron.MenuItemRoleClose},
},
},
})
// Retrieve a menu item// This will retrieve the "Checkbox 1" itemmi, _:=m.Item(1, 0)
// Add listener manually// An OnClick listener has already been added in the options directly for another menu itemmi.On(astilectron.EventNameMenuItemEventClicked, func(e astilectron.Event) bool {
log.Printf("Menu item has been clicked. 'Checked' status is now %t\n", *e.MenuItemOptions.Checked)
returnfalse
})
// Create the menum.Create()
// Manipulate a menu itemmi.SetChecked(true)
// Init a new menu itemvarni=m.NewItem(&astilectron.MenuItemOptions{
Label: astikit.StrPtr("Inserted"),
SubMenu: []*astilectron.MenuItemOptions{
{Label: astikit.StrPtr("Inserted 1")},
{Label: astikit.StrPtr("Inserted 2")},
},
})
// Insert the menu item at position "1"m.Insert(1, ni)
// Fetch a sub menus, _:=m.SubMenu(0)
// Init a new menu itemni=s.NewItem(&astilectron.MenuItemOptions{
Label: astikit.StrPtr("Appended"),
SubMenu: []*astilectron.MenuItemOptions{
{Label: astikit.StrPtr("Appended 1")},
{Label: astikit.StrPtr("Appended 2")},
},
})
// Append menu item dynamicallys.Append(ni)
// Pop up sub menu as a context menus.Popup(&astilectron.MenuPopupOptions{PositionOptions: astilectron.PositionOptions{X: astikit.IntPtr(50), Y: astikit.IntPtr(50)}})
// Close popups.ClosePopup()
// Destroy the menum.Destroy()
A few things to know:
when assigning a role to a menu item, go-astilectron won't be able to capture its click event
on MacOS there's no such thing as a window menu, only app menus therefore my advice is to stick to one global app menu instead of creating separate window menus
on MacOS MenuItem without SubMenu is not displayed
// Create the notificationvarn=a.NewNotification(&astilectron.NotificationOptions{
Body: "My Body",
HasReply: astikit.BoolPtr(true), // Only MacOSXIcon: "/path/to/icon",
ReplyPlaceholder: "type your reply here", // Only MacOSXTitle: "My title",
})
// Add listenersn.On(astilectron.EventNameNotificationEventClicked, func(e astilectron.Event) (deleteListenerbool) {
log.Println("the notification has been clicked!")
return
})
// Only for MacOSXn.On(astilectron.EventNameNotificationEventReplied, func(e astilectron.Event) (deleteListenerbool) {
log.Printf("the user has replied to the notification: %s\n", e.Reply)
return
})
// Create notificationn.Create()
// Show notificationn.Show()
Dock (MacOSX only)
// Get the dockvard=a.Dock()
// Hide and show the dockd.Hide()
d.Show()
// Make the Dock bounceid, _:=d.Bounce(astilectron.DockBounceTypeCritical)
// Cancel the bounced.CancelBounce(id)
// Update badge and icond.SetBadge("test")
d.SetIcon("/path/to/icon")
// New dock menuvarm=d.NewMenu([]*astilectron.MenuItemOptions{
{
Label: astikit.StrPtr("Root 1"),
SubMenu: []*astilectron.MenuItemOptions{
{Label: astikit.StrPtr("Item 1")},
{Label: astikit.StrPtr("Item 2")},
{Type: astilectron.MenuItemTypeSeparator},
{Label: astikit.StrPtr("Item 3")},
},
},
{
Label: astikit.StrPtr("Root 2"),
SubMenu: []*astilectron.MenuItemOptions{
{Label: astikit.StrPtr("Item 1")},
{Label: astikit.StrPtr("Item 2")},
},
},
})
// Create the menum.Create()
Dialogs
Add the following line at the top of your javascript file :
请发表评论