gallium

package module
Version: v0.0.0-...-29fad37 Latest Latest
Warning

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

Go to latest
Published: Mar 24, 2017 License: MIT Imports: 10 Imported by: 3

README

GoDoc Build Status

Write desktop applications in Go, HTML, Javascript, and CSS.

Gallium is a Go library for managing windows, menus, dock icons, and desktop notifications. Each window contains a webview component, in which you code your UI in HTML. Under the hood, the webview is running Chromium.

Warning

This is an extremely early version of Gallium. Most APIs will probably change before the 1.0 release, and much of the functionality that is already implemented remains unstable.

Platforms

Only OSX is supported right now. I intend to add support for Windows and Linux soon.

Discussion

Join the #gallium channel over at the Gophers slack. (You can request an invite to the Gophers slack team here.)

Installation

Requires go >= 1.7

First install git large file storage, then install Gallium:

$ brew install git-lfs
$ git lfs install
$ go get github.com/alexflint/gallium  # will not work without git lfs!

This will fetch a 92MB framework containing a binary distribution of the Chromium content module, so it may take a few moments. This is also why git large file storage must be installed (github has a limit on file size.)

Quickstart

package main

import (
  "os"
  "runtime"

  "github.com/alexflint/gallium"
)

func main() {
  runtime.LockOSThread()         // must be the first statement in main - see below
  gallium.Loop(os.Args, onReady) // must be called from main function
}

func onReady(app *gallium.App) {
  app.OpenWindow("http://example.com/", gallium.FramedWindow)
}

To run the example as a full-fledged UI application, you need to build an app bundle:

$ go build ./example
$ go install github.com/alexflint/gallium/cmd/gallium-bundle
$ gallium-bundle example
$ open example.app

Result of the example

If you run the executable directly without building an app bundle then many UI elements, such as menus, will not work correctly.

$ go run example.go

Menus

func main() {
  runtime.LockOSThread()
  gallium.Loop(os.Args, onReady)
}

func onReady(app *gallium.App) {
  app.OpenWindow("http://example.com/", gallium.FramedWindow)
  app.SetMenu([]gallium.Menu{
    gallium.Menu{
      Title: "demo",
      Entries: []gallium.MenuEntry{
        gallium.MenuItem{
          Title:    "About",
          OnClick:  handleMenuAbout,
        },
        gallium.Separator,
        gallium.MenuItem{
          Title:    "Quit",
          Shortcut: "Cmd+q",
          OnClick:  handleMenuQuit,
        },
      },
    },
  })
}

func handleMenuAbout() {
  log.Println("about clicked")
  os.Exit(0)
}

func handleMenuQuit() {
  log.Println("quit clicked")
  os.Exit(0)
}

Menu demo

Status Bar

func main() {
  runtime.LockOSThread()
  gallium.Loop(os.Args, onReady)
}

func onReady(app *gallium.App) {
  app.OpenWindow("http://example.com/", gallium.FramedWindow)
  app.AddStatusItem(
    20,
    "statusbar",
    true,
    gallium.MenuItem{
      Title:   "Do something",
      OnClick: handleDoSomething,
    },
    gallium.MenuItem{
      Title:   "Do something else",
      OnClick: handleDoSomethingElse,
    },
  )
}

func handleDoSomething() {
  log.Println("do something")
}

func handleDoSomethingElse() {
  log.Println("do something else")
}

Statusbar demo

Desktop Notifications

Note that the OSX Notification Center determines whether or not to show any given desktop notification, so you may need to open the notification center and scroll to the bottom in order to see notifications during development.

func main() {
  runtime.LockOSThread()
  gallium.Loop(os.Args, onReady)
}

func onReady(app *gallium.App) {
  img, err := gallium.ImageFromPNG(pngBuffer)
  if err != nil {
    ...
  }

  app.Post(gallium.Notification{
    Title:    "Wow this is a notification",
    Subtitle: "The subtitle",
    Image:    img,
  })
}

Dock icons

To add a dock icon, create a directory named myapp.iconset containing the following files:

icon_16x16.png          # 16 x 16
icon_16x16@2x.png       # 32 x 32
icon_32x32.png          # 32 x 32
icon_32x32@2x.png       # 64 x 64
icon_128x128.png        # 128 x 128
icon_128x128@2x.png     # 256 x 256
icon_256x256.png        # 256 x 256
icon_256x256@2x.png     # 512 x 512
icon_512x512.png        # 512 x 512
icon_512x512@2x.png     # 1024 x 1024

Then build you app with

gallium-bundle myapp --icon myapp.iconset

Alternatively, if you have a .icns file:

gallium-bundle myapp --icon myapp.icns

Writing native code

You can write C or Objective-C code that interfaces directly with native windowing APIs. The following example uses the macOS native API [NSWindow setAlphaValue] to create a semi-transparent window.

package main

import (
  "log"
  "os"
  "runtime"

  "github.com/alexflint/gallium"
)

/*
#cgo CFLAGS: -x objective-c
#cgo CFLAGS: -framework Cocoa
#cgo LDFLAGS: -framework Cocoa

#include <Cocoa/Cocoa.h>
#include <dispatch/dispatch.h>

void SetAlpha(void* window, float alpha) {
  // Cocoa requires that all UI operations happen on the main thread. Since
  // gallium.Loop will have initiated the Cocoa event loop, we can can use
  // dispatch_async to run code on the main thread.
  dispatch_async(dispatch_get_main_queue(), ^{
    NSWindow* w = (NSWindow*)window;
    [w setAlphaValue:alpha];
  });
}
*/
import "C"

func onReady(ui *gallium.App) {
  window, err := ui.OpenWindow("http://example.com/", gallium.FramedWindow)
  if err != nil {
    log.Fatal(err)
  }
  C.SetAlpha(window.NativeWindow(), 0.5)
}

func main() {
  runtime.LockOSThread()
  gallium.Loop(os.Args, onReady)
}

Relationship to other projects

Electron is a well-known framework for writing desktop applications in node.js. Electron and Gallium are similar in that the core UI is developed in HTML and javascript, but with Gallium the "outer layer" of logic is written in Go. Both Electron and Gallium use Chromium under the hood, and some of the C components for Gallium were ported from Electron.

The Chromium Embedded Framework is a C framework for embedding Chromium into other applications. I investigated CEF as a basis for Gallium but decided to use libchromiumcontent instead.

cef2go is a Go wrapper for Chromium based on CEF, but so far it still requires some manual steps to use as a library.

Rationale

The goal of Gallium is to make it possible to write cross-platform desktop UI applications in Go.

Troubleshooting

"file was built for unsupported file format"

If you see the following error:

ld: warning: ignoring file go/src/github.com/alexflint/gallium/dist/Gallium.framework/Gallium, file was built for unsupported file format ( 0x76 0x65 0x72 0x73 0x69 0x6F 0x6E 0x20 0x68 0x74 0x74 0x70 0x73 0x3A 0x2F 0x2F ) which is not the architecture being linked (x86_64): go/src/github.com/alexflint/gallium/dist/Gallium.framework/Gallium

then you probably have an issue with git lfs. You can confirm that this is the problem by checking the size of the file in the error message: it should be over 1 MB, but if you see a much smaller file then this is your problem.

To fix this, try re-installing git lfs as described in the installation section above, then delete and re-install gallium.

No console output

When you run an app bundle with open Foo.app, OSX launch services discards standard output and standard error. If you need to see this output for debugging purposes, use a redirect:

gallium.RedirectStdoutStderr("output.log")

App does not start

When you run an app bundle with open Foo.app, OSX launch services will only start your app if there is not already another instance of the same application running, so if your app refuses to start then try checking the activity monitor for an already running instance.

Menus not visible

If you run the binary directly without building an app bundle then your menus will not show up, and the window will initially appear behind other applications.

UI thread issues and runtime.LockOSThread

It is very important that the first statement in your main function be runtime.LockOSThread(). The reason is that gallium calls out to various C functions in order to create and manage OSX UI elements, and many of these are required to be called from the first thread created by the process. But the Go runtime creates many threads and any one piece of Go code could end up running on any thread. The solution is runtime.LockOSThread, which tells the Go scheduler to lock the current goroutine so that it will only ever run on the current thread. Since the main function always starts off on the main thread, this wil guarantee that the later call to gallium.Loop will also be on the main thread. At this point gallium takes ownership of this thread for its main event loop and calls the OnReady callback in a separate goroutine. From this point forward it is safe to call gallium functions from any goroutine.

Shared libraries and linking issues

Gallium is based on Chromium, which it accesses via Gallium.framework. That framework in turn contains libchromiumcontent.dylib, which is a shared library containing the chromium content module and is distributed in binary form by the same folks responsible for the excellent Electron framework. When you build your Go executable, the directives in Gallium.framework instruct the linker to set up the executable to look for Gallium.framework in two places at runtime:

  1. <dir containing executable>/../Frameworks/Gallium.framework: this will resolve correctly if you choose to build and run your app as a bundle (and also means you can distribute the app bundle as a self-contained unit).
  2. $GOPATH/src/github.com/alexflint/dist/Gallium.framework: this will resolve if you choose to run your executable directly.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrImageDecodeFailed = errors.New("image could not be decoded")
)
View Source
var FramedWindow = WindowOptions{
	Shape: Rect{
		Width:  800,
		Height: 600,
		Left:   100,
		Bottom: 100,
	},
	TitleBar:         true,
	Frame:            true,
	Resizable:        true,
	CloseButton:      true,
	MinButton:        true,
	FullScreenButton: true,
	Title:            defaultWindowTitle(),
}

FramedWindow contains options for an "ordinary" window with title bar, frame, and min/max/close buttons.

View Source
var FramelessWindow = WindowOptions{
	Shape: Rect{
		Width:  800,
		Height: 600,
		Left:   100,
		Bottom: 100,
	},
	Resizable: true,
}

FramelessWindow contains options for a window with no frame or border, but that is still resizable.

Functions

func AddGlobalShortcut

func AddGlobalShortcut(keys KeyCombination, handler func())

AddGlobalShortcut calls the handler whenever the key combination is pressed in any application.

func BundleInfo

func BundleInfo(key string) string

BundleInfo looks up an entry in the Info.plist for the current bundle.

func Loop

func Loop(args []string, onReady func(*App)) error

Loop starts the browser loop and does not return unless there is an initialization error

func RedirectStderr

func RedirectStderr(path string) (*os.File, error)

RedirectStderr overwrites the stderr streams with a file descriptor that writes to the given path. This is done with os.Dup2, meaning that even C functions that write to stderr will be redirected to the file.

func RedirectStdout

func RedirectStdout(path string) (*os.File, error)

RedirectStdout overwrites the stdout streams with a file descriptor that writes to the given path. This is done with os.Dup2, meaning that even C functions that write to stdout will be redirected to the file.

func RedirectStdoutStderr

func RedirectStdoutStderr(path string) (*os.File, error)

RedirectStdoutStderr overwrites the stdout and stderr streams with a file descriptor that writes to the given path. This is done with os.Dup2, meaning that even C functions that write to stdout or stderr will be redirected to the file.

func RunApplication

func RunApplication()

RunApplication is for debugging only. It allows creation of menus and desktop notifications without firing up any parts of chromium. It will be removed before the 1.0 release.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the handle that allows you to create windows and menus

func (*App) AddStatusItem

func (app *App) AddStatusItem(opts StatusItemOptions)

func (*App) OpenWindow

func (app *App) OpenWindow(url string, opt WindowOptions) (*Window, error)

OpenWindow creates a window that will load the given URL.

func (*App) Post

func (app *App) Post(n Notification)

Post shows the given desktop notification

func (*App) SetMenu

func (app *App) SetMenu(menus []Menu)

type Image

type Image struct {
	// contains filtered or unexported fields
}

Image holds a handle to a platform-specific image structure (e.g. NSImage on macOS).

func ImageFromPNG

func ImageFromPNG(buf []byte) (*Image, error)

ImageFromPNG creates an image from a buffer containing a PNG-encoded image.

type KeyCombination

type KeyCombination struct {
	Key       string
	Modifiers Modifier
}

KeyCombination represents a key together with zero or more modifiers. It is used to set up keyboard shortcuts.

func MustParseKeys

func MustParseKeys(s string) KeyCombination

MustParseKeys is like ParseKeys but panics on error

func ParseKeys

func ParseKeys(s string) (KeyCombination, error)

ParseKeys parses a key combination specified as a string like "cmd shift a".

type Menu struct {
	Title   string
	Entries []MenuEntry
}

A Menu has a title and a list of entries. It is a non-leaf node in the menu tree.

type MenuEntry interface {
	// contains filtered or unexported methods
}

MenuEntry is the interface for menus and menu items.

var Separator MenuEntry

Separator displays a horizontal separator within a menu

type MenuItem struct {
	Title    string
	Shortcut KeyCombination
	OnClick  func()
}

A MenuItem has a title and can be clicked on. It is a leaf node in the menu tree.

type Modifier

type Modifier int

Modifier represents zero or more modifier keys (control, shift, option, etc)

const (
	ModifierCmd Modifier = 1 << iota
	ModifierCtrl
	ModifierCmdOrCtrl
	ModifierAltOrOption
	ModifierFn
	ModifierShift
)

these must be kept in sync with the gallium_modifier_t enum in cocoa.h

type Notification

type Notification struct {
	Title             string
	Subtitle          string
	InformativeText   string
	Image             *Image
	Identifier        string
	ActionButtonTitle string
	OtherButtonTitle  string
}

Notification represents a desktop notification

type Rect

type Rect struct {
	Width  int // Width in pixels
	Height int // Height in pixels
	Left   int // Left is offset from left in pixel
	Bottom int // Left is offset from top in pixels
}

Rect represents a rectangular region on the screen

type Screen

type Screen struct {
	Shape        Rect // the size and position of this screen in global coords
	Usable       Rect // excludes the menubar and dock
	BitsPerPixel int  // color depth of this screen (total of all color components)
	ID           int  // unique identifier for this screen
}

A screen represents a rectangular display, normally corresponding to a physical display. "Device coordinates" means a position on a screen measured from (0, 0) at the bottom left of the device. "Global coordinates" means the coordinate system in which each of the screens are positioned relative to each other. Global and device coordinates almost always have the same scale factor. It is possible for screens to overlap in global coordinates (such as when mirroring a display.)

func FocusedScreen

func FocusedScreen() Screen

FocusedScreen gets the screen containing the currently focused window

func Screens

func Screens() []Screen

Screens gets a list of available screens

type StatusItemOptions

type StatusItemOptions struct {
	Image     *Image      // image to show in the status bar, must be non-nil
	Width     float64     // width of item in pixels (zero means automatic size)
	Highlight bool        // whether to highlight the item when clicked
	Menu      []MenuEntry // the menu to display when the item is clicked
}

type Window

type Window struct {
	// contains filtered or unexported fields
}

Window represents a window registered with the native UI toolkit (e.g. NSWindow on macOS)

func (*Window) Close

func (w *Window) Close()

Close closes the window, as if the close button had been clicked.

func (*Window) CloseDevTools

func (w *Window) CloseDevTools()

CloseDevTools closes the developer tools.

func (*Window) Copy

func (w *Window) Copy()

Copy copies the current text selection to the pasteboard

func (*Window) Cut

func (w *Window) Cut()

Cut cuts the current text selection to the pastboard

func (*Window) Delete

func (w *Window) Delete()

Delete deletes the current text selection

func (*Window) DevToolsAreOpen

func (w *Window) DevToolsAreOpen() bool

DevToolsVisible returns whether the developer tools are showing

func (*Window) LoadURL

func (w *Window) LoadURL(url string)

LoadURL causes the window to load the given URL

func (*Window) Miniaturize

func (w *Window) Miniaturize()

Miniaturize miniaturizes the window, as if the min button had been clicked.

func (*Window) NativeController

func (w *Window) NativeController() unsafe.Pointer

NativeWindow gets an operating-system dependent handle for the window controller. Under macOS this is *NSWindowController.

func (*Window) NativeWindow

func (w *Window) NativeWindow() unsafe.Pointer

NativeWindow gets a operating-system dependent handle for this window. Under macOS this is NSWindow*.

func (*Window) Open

func (w *Window) Open()

Open opens the window. This is the default state for a window created via OpenWindow, so you only need to call this if you manually closed the window.

func (*Window) OpenDevTools

func (w *Window) OpenDevTools()

OpenDevTools opens the developer tools for this window.

func (*Window) Paste

func (w *Window) Paste()

Paste pastes from the pasteboard

func (*Window) PasteAndMatchStyle

func (w *Window) PasteAndMatchStyle()

PasteAndMatchStyle pastes from the pasteboard, matching style to the current element

func (*Window) Redo

func (w *Window) Redo()

Redo redoes the last text editing action

func (*Window) Reload

func (w *Window) Reload()

Reload reloads the current URL

func (*Window) ReloadNoCache

func (w *Window) ReloadNoCache()

Reload reloads the current URL, ignoring cached versions of resources.

func (*Window) SelectAll

func (w *Window) SelectAll()

SelectAll selects all text in the current element

func (*Window) SetShape

func (w *Window) SetShape(r Rect)

Shape gets the current shape of the window.

func (*Window) Shape

func (w *Window) Shape() Rect

Shape gets the current shape of the window.

func (*Window) URL

func (w *Window) URL() string

URL gets the URL that the window is currently at.

func (*Window) Undo

func (w *Window) Undo()

Undo undoes the last text editing action

func (*Window) Unselect

func (w *Window) Unselect()

Unselect unselects any text selection

type WindowOptions

type WindowOptions struct {
	Title            string // String to display in title bar
	Shape            Rect   // Initial size and position of window
	TitleBar         bool   // Whether the window title bar
	Frame            bool   // Whether the window has a frame
	Resizable        bool   // Whether the window border can be dragged to change its shape
	CloseButton      bool   // Whether the window has a close button
	MinButton        bool   // Whether the window has a miniaturize button
	FullScreenButton bool   // Whether the window has a full screen button
	Menu             []MenuEntry
}

WindowOptions contains options for creating windows

Jump to

Keyboard shortcuts

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