appicon

package module
v0.3.0 Latest Latest
Warning

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

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

README

go-macos/appicon

ci Go Reference License

A running application's own icon, as pixels, from pure Go with CGO_ENABLED=0. No cgo, no sips, no Objective-C source file anywhere in the build: it reaches AppKit through go-macos/objc, which reaches it through purego.

px, err := appicon.ForPID(pid, 64)   // 64x64, straight RGBA
if err != nil {
        return err
}
cell.Image = toolkit.NewImageRGBA(px.Pix, px.W, px.H)

It exists because an interface that offers a person a list of what is running — a switcher, a gallery, a picker for which window goes where — is a list of names until it has the icons, and a name is not how anybody recognises an application. Every system that shows one shows the icon: the Dock, ⌘-Tab, Mission Control.

The icon belongs to whoever wrote the application. This package does not draw one, guess one, or ship artwork: it asks for the icon that application is already showing in the Dock.

No permission of any kind. An application's icon is public information about a running process, unlike its windows — no Accessibility grant, no Screen Recording, no prompt.

A size is asked for, not inferred

An icon is a family of representations, not a picture — 32 of them for Firefox on the machine this was written on. Asking for 64 gets the 64 the designer drew; taking "the" icon and scaling it gets a blurred 512.

What was measured, not assumed

Three things went wrong on the way here, and each is a comment in the code now:

NSRunningApplication answers from a CACHE. It is maintained by run-loop notifications, so a process that never runs one asks a list filled before it started and gets nil for an application that is plainly running — Firefox, pid 12849, "no such application". The run loop is pumped first, through objc.PumpRunLoop.

A class that was never loaded is not an error. ClassID answers zero and every message to it answers zero, which arrives as "no such application" rather than as "AppKit is not open". It is loaded explicitly.

A bitmap context cannot draw non-premultiplied. Asking for NSAlphaNonpremultiplied makes +graphicsContextWithBitmapImageRep: return nil — found with a probe that printed each step: the rep was fine, its bitmapData was fine, the context was nil. So the icon is drawn premultiplied and undone on the way out, because Pixels promises straight RGBA — what a PNG holds and what an image widget takes. Handing out premultiplied pixels as straight ones darkens exactly the transparent corners.

Tests

go test ./...                        # portable logic
APPICON_LIVE=1 go test -run Live     # + the window server

The live test reads the icon of whatever application is frontmost — the Finder when nothing else — and asserts the pixels are neither empty nor grey: an icon that rasterised to nothing would pass every other check. A test binary cannot read its own icon, and that is worth knowing rather than working around: a plain Go binary has no bundle and an activation policy of Prohibited, so AppKit does not know it exists.

Requirements

macOS, Go 1.24+, CGO_ENABLED=0. Off darwin every entry point answers ErrUnsupported, so a consumer that cross-compiles gets one clean error instead of a missing symbol.

Licence

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package appicon reads a running application's own icon, as pixels, from pure Go with CGO_ENABLED=0.

It exists because an interface that offers a person a list of what is running — a switcher, a gallery, a picker for which window goes where — is a list of NAMES until it has the icons, and a name is not how anybody recognises an application. Every system that shows one shows the icon: the Dock, ⌘-Tab, Mission Control.

The icon belongs to whoever wrote the application. This package does not draw one, guess one, or ship artwork: it asks the window server for the icon that application is already showing in the Dock, and hands over the pixels.

px, err := appicon.ForPID(pid, 64)
if err == nil {
	cell.Image = toolkit.NewImageRGBA(px.Pix, px.W, px.H)
}

It needs no permission of any kind: NSRunningApplication's icon is public information about a running process, unlike its windows.

Index

Constants

View Source
const (
	MinSize = 8
	MaxSize = 1024
)

The sizes an icon may be asked for.

The floor is where an icon stops being recognisable and the ceiling is what macOS itself stores: an .icns holds 1024 at the largest, and asking for more scales a picture up rather than giving a better one.

Variables

View Source
var (
	// ErrUnsupported is what every entry point answers away from macOS.
	ErrUnsupported = errors.New("appicon: unsupported on this platform (macOS only)")
	// ErrNoApp means no running application owns that process id. A shell, a
	// daemon and a process that has already exited all look like this.
	ErrNoApp = errors.New("appicon: no running application with that process id")
	// ErrNoIcon means the application has one but it could not be rasterised —
	// an icon that is entirely a vector at a size nothing has drawn yet, or a
	// bitmap context the window server would not give.
	ErrNoIcon = errors.New("appicon: the application's icon could not be read")
	// ErrSize means a size outside [MinSize, MaxSize] was asked for.
	ErrSize = errors.New("appicon: unusable size")
)

Errors this package returns.

View Source
var ErrNoSymbol = errors.New("appicon: no such system symbol")

ErrNoSymbol means this system has no symbol of that name, or the name is not one.

Functions

This section is empty.

Types

type Pixels

type Pixels struct {
	Pix  []byte
	W, H int
}

Pixels is a rasterised icon: straight RGBA, 8 bits a channel, W*H*4 bytes, rows top to bottom with no padding.

Straight and not premultiplied, because that is what a toolkit's image widget takes and what a PNG holds; the conversion is done here, once, rather than left as a surprise in whatever draws it.

func ForPID

func ForPID(pid int32, size int) (Pixels, error)

ForPID returns the icon of the application owning pid, rasterised square at size pixels a side.

A size is asked for rather than inferred because an icon is a family of representations, not a picture: asking for 64 gets the 64 the designer drew, where taking "the" icon and scaling it gets a blurred 512.

func Symbol added in v0.2.0

func Symbol(name string, size int) (Pixels, error)

Symbol renders one of the system's own symbols, at its own shape, no bigger than size pixels on its longer side.

It is here rather than in a package of its own because the hard half is already here: turning an NSImage into straight RGBA without cgo. An SF Symbol IS an NSImage; only the way of asking for one differs.

It exists because a menu-bar icon has to be legible at about 22 points, and a glyph drawn as an outline by a cross-platform toolkit is not the same thing as one the system draws for its own bar. MEASURED, in the item's own strip of a real menu bar: an emoji title puts 79 pixels of ink there, a toolkit's glasses outline 140 at twice the size, and the system's own "visionpro" 182.

The name is an SF Symbol's, such as "eyeglasses" or "display". A name the system does not have reports ErrNoSymbol rather than a blank picture, because a menu bar with a hole in it is worse than one with nothing in it.

Jump to

Keyboard shortcuts

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