Documentation
¶
Overview ¶
Package desk shows several captured computer screens floating on a 360° ribbon inside AR glasses, scrolled from the keyboard.
Index ¶
- Constants
- Variables
- func Await(ctx context.Context, opt AwaitOptions) (glasses.Display, error)
- func BadgeDuration(seconds float64) time.Duration
- func Billboards() []glasses.USB
- func BringPointer([]uint64, int) error
- func ConfigPath() (string, error)
- func DarkNotePath() (string, error)
- func DisplayAt(x, y, w, h float64) (uint64, bool)
- func DisplayOf(o Offer) (uint64, bool)
- func DisplayOfferID(id uint64) string
- func EvidenceFor(d glasses.Display, _ bool, us []glasses.USB) *glasses.USB
- func FitScale(want float64, maxW, maxH int, size func(float64) (int, int)) float64
- func Mirrors(on []Offer, ours []uint64, own uint64) []uint64
- func OwnDisplay(name string) (uint64, bool)
- func Peripherals() []glasses.USB
- func PhotoPath(at time.Time) (string, error)
- func PointerHome() func()
- func PutBackWhatWasLeftDark() ([]string, error)
- func RememberScreens(path string, n int) error
- func Run(ctx context.Context, plan Plan, d *Desk, opt RunOptions) error
- func RunSettings(opt SettingsOptions) error
- func Safely(logf func(string, ...any), fn func()) (err error)
- func Send(b Bench, ids []uint64, places []Placement) ([]string, error)
- func SettingsScale(displayH int) float64
- func ShouldChoose(cfg Config, screen string, attached []glasses.USB) bool
- func TakePhoto(camera string, logf func(string, ...any)) (string, error)
- func TrayIcon(px int, live bool) ([]byte, error)
- func WritePhoto(p Picture, at time.Time) (string, error)
- type Action
- type App
- type AwaitOptions
- type Bench
- type Canvas
- type Closer
- type Config
- func (c Config) BadgeSeconds() float64
- func (c Config) Bytes() []byte
- func (c Config) Distance() float64
- func (c Config) HotkeyOptions() *hotkey.Options
- func (c Config) Immersive() bool
- func (c Config) Ladder() []hotkey.Modifier
- func (c Config) Mirror() bool
- func (c Config) Model() string
- func (c Config) Placements() []Placement
- func (c Config) Save() (string, error)
- func (c Config) SaveTo(path string) error
- func (c Config) Screens() int
- func (c Config) ShortcutsOr(base []Shortcut) []Shortcut
- func (c Config) SplayDeg() float64
- type ConfigGlasses
- type ConfigPlace
- type ConfigRibbon
- type ConfigShortcut
- type Desk
- func (d *Desk) Advance(dt float64)
- func (d *Desk) Badge(seconds float64, theme *toolkit.Theme)
- func (d *Desk) Canvas() *Canvas
- func (d *Desk) Click(x, y int) bool
- func (d *Desk) Close() error
- func (d *Desk) Distance() float64
- func (d *Desk) Do(a Action)
- func (d *Desk) Err() error
- func (d *Desk) FeedAt(i int) Feed
- func (d *Desk) Focus() int
- func (d *Desk) Grow(f Feed) (int, error)
- func (d *Desk) InGallery() bool
- func (d *Desk) Look(pos int) error
- func (d *Desk) Nav() *ribbon.Nav
- func (d *Desk) Plan() Plan
- func (d *Desk) Quit() bool
- func (d *Desk) Render() *Canvas
- func (d *Desk) SetFeed(i int, f Feed) (Feed, error)
- func (d *Desk) Shrink(pos int) (Feed, error)
- func (d *Desk) WantsSettings() bool
- type Dimmer
- type Fan
- type Feed
- type Fence
- type Gestures
- type Grid
- func (g *Grid) Adder() (int, bool)
- func (g *Grid) At(x, y int) (int, bool)
- func (g *Grid) Cell(i int) (x, y, w, h int, ok bool)
- func (g *Grid) Cells() int
- func (g *Grid) Frame(dst []ribbon.Blit) []ribbon.Blit
- func (g *Grid) IsAdder(i int) bool
- func (g *Grid) Len() int
- func (g *Grid) Move(d ribbon.Direction) error
- func (g *Grid) Screens() int
- func (g *Grid) Select(i int) error
- func (g *Grid) Selected() int
- func (g *Grid) SetSourceWidths(w []int)
- func (g *Grid) Shape() (cols, rows int)
- type Hotkeys
- type Icon
- type Inventory
- func (inv *Inventory) Assign(pos int, id string) error
- func (inv *Inventory) At(pos int) (Offer, bool)
- func (inv *Inventory) Clear(pos int) error
- func (inv *Inventory) Cycle(pos int) (Offer, bool)
- func (inv *Inventory) Describe() string
- func (inv *Inventory) Offers() []Offer
- func (inv *Inventory) Positions() int
- func (inv *Inventory) Unused() []Offer
- func (inv *Inventory) Where(id string) int
- type Kind
- type Offer
- type Options
- type Picture
- type Placement
- type Plan
- func (p Plan) Count() int
- func (p Plan) Distance() float64
- func (p Plan) ScreenWidth(i int) int
- func (p Plan) Screens() []ribbon.Screen
- func (p Plan) SplayDeg() float64
- func (p Plan) String() string
- func (p Plan) WithDistance(d float64) Plan
- func (p Plan) WithScreenWidth(i, w int) Plan
- func (p Plan) WithScreens(n int) Plan
- func (p Plan) WithSplay(deg float64) Plan
- type RunOptions
- type Screens
- type SettingsOptions
- type Shortcut
- type Slant
- type SlantCol
- type Source
- type Strip
- func (s *Strip) Frame(dst []ribbon.Blit, offset int) []ribbon.Blit
- func (s *Strip) Fullscreen(dst []ribbon.Blit, i int) ([]ribbon.Blit, error)
- func (s *Strip) Offset(yaw float64) int
- func (s *Strip) Screens() int
- func (s *Strip) SetSourceWidths(w []int)
- func (s *Strip) Toward(yaw float64, focus int) float64
- func (s *Strip) Width() int
- type Tray
- type TrayRow
- type TrayState
Constants ¶
const (
MinAspectNum, MinAspectDen = 1, 4
MaxAspectNum, MaxAspectDen = 8, 1
)
The shapes a screen is allowed to take, as fractions of its height: from a tall panel on its side to an ultrawide. A source outside that is not a shape somebody chose, it is a capture that has gone wrong, and giving the band a screen a hundred times too wide would leave nothing else visible.
const ( // SettingsWidth is how wide the window is. Its HEIGHT is measured, not // declared -- see settingsSize. SettingsWidth = 560 // SettingsPadX and SettingsPadY are the window's margins. SettingsPadX = 16 SettingsPadY = 12 // ButtonBarH is the band the Save/Close row keeps at the bottom. It is a // BorderLayout region, so the content above it can never reach into it -- // which is what a fixed-height run of fields used to do when the window was // made smaller: the buttons ended up drawn over the text. ButtonBarH = 40 // ButtonW is one button. ButtonW = 96 // ControlW and ControlH size the trailing control of a // settings row. A SettingRow right-aligns a control at the size the control // carries, so these are given to the widget rather than to the row. ControlW = 220 ControlH = 28 // SwitchW and SwitchH size a switch, which is wider than it is tall. SwitchW = 44 SwitchH = 24 // PageSpacing is the gap between the cards on the page. PageSpacing = 12 // GlassesIconPx is the drawn headset in a glasses tile. Bigger than a // toolbar icon, because the tile is the thing being chosen rather than a // decoration beside a word. GlassesIconPx = 40 // GlassesTileW is the floor on a tile's width, in logical pixels: enough for // the longest model name in the catalogue rather than for the icon. GlassesTileW = 150 )
Sizes of the settings window and its controls, in LOGICAL pixels: everything here goes through toolkit.Scaled at use, so one knob magnifies the whole window and nothing is a device-pixel constant.
const ( TraySymbol = "visionpro" TrayLabel = "XR desk" )
TraySymbol is the system symbol the item shows in the menu bar, and TrayLabel is what a screen reader says about it.
A SYMBOL RATHER THAN AN EMOJI, and it was measured rather than argued. In the item's own strip of the menu bar:
emoji title 👓 79 pixels of ink symbol eyeglasses 100 symbol rectangle.3.group 161 symbol visionpro 182 symbol display 206
"the glasses icon in the tray is not very legible" is what the first of those looks like to somebody using it: a title is TEXT, so an emoji arrives at the height of a lowercase letter, flat in the bar's own ink, among twenty other items. A symbol is vector, aligned to the bar's cap height, and takes the bar's appearance in every theme.
visionpro rather than the darkest of them: a headset says what this program is, where a monitor says what everything else is. eyeglasses is the shape of the old icon and half the ink, which is the trade this was reported for.
const AppIconPx = 48
AppIconPx is the SMALLEST an application tile's icon may be, in pixels.
The size actually used comes from the height of the picture — see appsIconPx — because a tile read at arm's length through a pair of glasses is not a tile read on a monitor. This is the floor under that, for a picture small enough that a twelfth of it would be nothing.
const AwaitPoll = time.Second
AwaitPoll is how often Await asks again.
A second, because that is under the time it takes to look up after pushing a plug in, and because listing displays is cheap. Measured: a display created halfway through a process with no NSApp running appeared in the list within 500 ms, so a second is a poll that cannot miss it and not a poll that is waiting on a cache.
const DefaultBadgeSeconds = 1.5
DefaultBadgeSeconds is how long the screen's number stays up after the band moves.
Long enough to read without looking for it, short enough that it is gone before it becomes part of the picture. Zero seconds turns it off.
const DefaultColumns = 3
DefaultColumns is how wide the gallery is when nobody says.
Three, whenever three columns hold every screen in three rows or fewer — which is exactly the three, six and nine a desk is usually built from. A FIXED width is the point: a screen keeps its place in the grid as others are added, so the map a person builds of where things are survives the desk growing. Past nine it stops paying, and the shape is chosen by what leaves the screens biggest instead.
const DefaultFOVDeg = 45
DefaultFOVDeg is the field of view assumed when the headset's is not known.
The plan reports a field of view and does not require one, on purpose: what fills the glasses is one source pixel per panel pixel, which needs a resolution and nothing else. But the moment screens are TURNED, the angle matters -- it decides how much a panel off to the side is stretched by a flat projection -- so a number is needed and this is the one.
Forty-five degrees, which is close to what the headsets in the catalogue actually have (44.9° for a Luma Ultra, 45.6° for a Beast). Getting it wrong does not move the screen in front of the viewer by a pixel; it makes the ones beside it slightly more or slightly less stretched than they should be.
const DefaultGapPx = 48
DefaultGapPx is the band the gallery leaves between two cells, in pixels.
The ribbon needs no such number — the space between two screens is whatever the ribbon left there — but a grid is a fold this package invents, so the space in it is this package's to choose. Wide enough that two cells read as two screens rather than as one wide one.
const DefaultNoticeSeconds = 2.5
DefaultNoticeSeconds is how long a notice stays up when nothing takes it down. Long enough to read a short sentence, short enough not to become part of the picture.
const DefaultScreens = 6
DefaultScreens is how many virtual screens a desk gets when nobody says.
Six: two rows of three in the gallery, which is a shape a person keeps a map of, and enough screens that the band is worth turning. It used to be "as many as fit round the circle", which was a real limit when the screens were curved and is a fiction now that they are flat.
const DefaultSplayDeg = 20.0
DefaultSplayDeg is the angle a desk gets when nobody says: twenty degrees.
Enough to read as turned -- the keystone is visible, the neighbours face you -- and shallow enough that a screen two along is still squarely in front of you when you turn to it. It is also close to what people set real monitors to, which is the only evidence available for a number like this.
const DistanceStep = 0.25
DistanceStep is how much one press moves the band.
A quarter, so the near end and the far end are twelve presses apart: enough that a person can stop where they want, few enough that they can get there.
const EnvConfig = "XRDESK_CONFIG"
EnvConfig names the environment variable that overrides which file the settings are read from. It exists so a test, or a person trying something out, can point at a scratch file without touching the real one.
const FanReach = 4
FanReach is how many panels either side of the middle one a frame considers.
Four, which is more than a rectilinear projection can ever show: at the far end of the distance range four screens span the view, so the fifth is off the edge whatever the splay. Considering one too many costs a projection that comes back refused; considering one too few loses a screen that should be in shot, and nothing would say so.
const FrameInterval = 16 * time.Millisecond
FrameInterval is how often the ribbon is advanced and redrawn.
It is not tied to the display's refresh rate. The captures are change-driven and mostly still, the warp costs 2.8 ms and the composite under one, so drawing more often would spend the budget re-deriving a picture nobody changed.
const MaxDistance = 4.0
MaxDistance is the far end: four screens across the view.
A float, because it is compared with and assigned to a distance and an untyped integer constant next to a float64 is a conversion waiting to be forgotten.
Chosen, like MaxScreens, and for the same kind of reason rather than a geometric one. At four, a 1920-pixel screen is 480 pixels of a 1920-pixel view: text on it is a texture, not words. Somebody who wants the whole desk at once has the gallery, which draws every screen at a readable size instead of pretending nine of them fit in one view.
const MaxScreens = 9
MaxScreens is the most screens a desk carries: nine.
It is an ARBITRARY ceiling, chosen rather than derived, and that is worth saying plainly because nothing in this package supplies one. The screens are flat, so the band is as long as it needs to be; the plan would spread forty of them over the turn without complaint, and a test used to.
Nine is where three things happen to agree:
- the gallery is DefaultColumns wide, so nine fills three rows of three exactly, with nothing ragged and every screen keeping its column as the desk grows;
- a screen costs a display to create and a stream to capture, and that cost is linear — on macOS it is a CGVirtualDisplay each;
- past nine, a person stops holding a map of where things are, which is the whole point of a fixed arrangement.
A number asked for above it is CLAMPED rather than refused when it comes from the geometry (Plan.WithScreens), and REFUSED with the ceiling named when it comes from a person's settings file — a clamp is right for a program composing a plan and wrong for a line somebody wrote and expects to mean something.
const MaxSlantDeg = 80
MaxSlantDeg is how far a panel may be turned before it is dropped from the frame.
Eighty degrees, not ninety. At ninety a panel is edge-on: it projects to a line one column wide, and drawing it is a column of stretched pixels that reads as a scratch on the picture. The last ten degrees are worth nothing and cost a division by a depth approaching zero.
const MaxSplayDeg = 60.0
MaxSplayDeg is the widest angle between neighbours.
Sixty. Past it a chain of nine screens wraps round past the viewer's own shoulders and the far ones come back into shot from behind, which is not a desk -- and a panel turned that far is mostly edge, so its pixels are a smear whatever else is true. Chosen, like every other end in this package, and said so rather than derived.
const MinDistance = 1.0
MinDistance is the near end: one screen filling the view exactly, at one source pixel per output pixel.
It is the largest a screen can be shown in a given pair of glasses, which is what ActionFit returns to. Closer than this means seeing part of a screen, so the scale stops here rather than continuing into a crop.
const PhotoDirEnv = "XRDESK_PHOTO_DIR"
PhotoDirEnv moves where photographs are written. It is checked like any other choice: see PhotoPath.
const PhotoWait = 5 * time.Second
PhotoWait is how long to wait for the first frame after a camera is opened.
A camera is not instant: the session starts, the sensor powers up, the light comes on, and the first frame arrives a moment later -- measured at a few hundred milliseconds on the machine this was written on, and a headset over USB is slower than a built-in one. Long enough not to give up on a slow camera, short enough that somebody who pressed the key knows it has failed.
const PhotoWarmUp = 500 * time.Millisecond
PhotoWarmUp is how long the camera runs before the picture is taken.
A sensor powers up with its exposure at nothing and ramps, so the first frames a camera delivers are darker than what it can see. Half a second is what a camera application usually discards, and it is what this waits.
⚠ NOT MEASURED HERE, and it is worth being exact about why. A photograph taken from the first frame did come back 1920x1080 with a mean luminance of ZERO -- and so did the raw capture probe run against the same camera a minute later, which is the CONTROL that says the room was dark and not that the first frame was. The effect this guards against is real in general and was not the cause of what was seen, so this is a precaution and not a fix.
It is a WAIT rather than a test of the pixels, deliberately: a photograph of something genuinely dark is a photograph, and refusing it would be worse than taking it.
const SelectionWidth = 8
SelectionWidth is how thick that border is, in logical pixels.
Thick. A one-pixel ring round a tile that subtends a hand's width in a headset is a rumour; this is the whole answer to "which one am I about to choose", so it is drawn like one.
const SettingsFontPx = 13
SettingsFontPx is the settings window's type size in LOGICAL pixels, before the metric scale. Thirteen is a comfortable interface size; on a 2160-row panel the scale takes it to thirty-nine.
const SettingsRoom = 0.9
SettingsRoom is the fraction of a display's usable area the settings window may occupy. A dialogue that reaches the very edges of the screen reads as a mistake, and on macOS the bottom edge is where the Dock lives.
const SplayStep = 5.0
SplayStep is how much one press changes the angle: five degrees, so the whole range is twelve presses and each one is visible.
const SysfsUSB = "/sys/bus/usb/devices"
SysfsUSB is where Linux publishes what it enumerated.
const TrayIconPx = 44
TrayIconPx is the icon's LONGER side, in pixels.
Twice the menu bar's own height, so it is still sharp on a display that draws two pixels per point. The other side follows the glyph: a system symbol is not square -- visionpro is 21 by 13 points -- and the bar scales what it is given by height, so a picture forced square would show the glyph stretched.
const TrayQueue = 4
TrayQueue is how many menu choices are held for the run loop.
It is small on purpose: the loop reads continuously while the desk is up, so the only time anything queues is while the settings window has the desk stopped -- and a person clicking the menu five times then means the fifth click, not five actions replayed when the desk comes back.
const TrayRowPx = 32
TrayRowPx is a menu ROW's glyph, in pixels.
go-widgets/tray draws a row's icon at 16 points, so 32 pixels is that at 2x and a Retina display gets one image pixel per device pixel with nothing resampled. It is NOT TrayIconPx: a menu-bar icon sits in a 22-point bar and a row's icon sits beside the row's text, and a symbol rasterised for one and drawn at the other has strokes of the wrong weight.
const TrayTitle = "\U0001F453"
TrayTitle is the desk in one glyph, for anywhere a picture will not do -- a log line, a window title, a person asking what to look for in their menu bar.
const TrayTooltip = "XR desk"
TrayTooltip is what the item says when somebody rests on it.
Variables ¶
var ( // ErrAwaitQuit says the person asked to quit while waiting. ErrAwaitQuit = errors.New("desk: asked to quit while waiting for a display") // ErrAwaitSettings says the person asked for the settings while waiting. // The desk has nothing to show yet, and the settings are exactly where // somebody goes when the glasses are not being found. ErrAwaitSettings = errors.New("desk: asked for the settings while waiting for a display") )
What Await stopped for, when it did not stop because a display arrived.
var ( // ErrNoSuchOffer means nothing in the inventory has that identifier. ErrNoSuchOffer = errors.New("desk: no such source") // ErrPosition means the ribbon has no such position. ErrPosition = errors.New("desk: no such ribbon position") // ErrAlreadyShown means the source is already on another position. ErrAlreadyShown = errors.New("desk: that source is already on the ribbon") // ErrNoApps means no application has a window to show. ErrNoApps = errors.New("desk: no application with a window") )
Errors an inventory can refuse with.
var ( // ErrScreens means the requested number of screens is not usable. ErrScreens = errors.New("desk: unusable number of screens") // ErrFOV means the field of view given is not one. ErrFOV = errors.New("desk: unusable field of view") )
Errors a plan can refuse with.
var DefaultBackground = [4]byte{12, 14, 18, 255}
DefaultBackground is a near-black that is not black, so a gap between screens reads as part of the arrangement rather than as a dead panel.
var DefaultLadder = []hotkey.Modifier{ hotkey.Control, hotkey.Shift, hotkey.Control | hotkey.Shift, }
DefaultLadder is what is added to a taken combination, in order.
Control before Shift, deliberately. ⌥⌘Space is the Finder's search window on a stock macOS, so the gallery always falls back — and ⌃⌥⌘Space is both freer on a typical machine and easier to hold than ⌥⇧⌘Space, which puts three modifiers under one hand.
var DotInk = SelectionInk
DotInk is the colour of the light when a desk is up.
A green, and the same green the gallery uses for the screen it has chosen: one program, one word for "this one is live".
var ErrConfig = errors.New("desk: unusable settings")
ErrConfig says a settings file could not be used.
var ErrCrashed = errors.New("desk: the frame loop crashed")
ErrCrashed wraps whatever a frame loop panicked with.
var ErrNoDimming = errors.New("desk: darkening a display is not available on this platform")
ErrNoDimming is what a platform with no backlight control answers.
var ErrNoPointer = errors.New("desk: no way to move the pointer on this platform")
ErrNoPointer means this platform has no way for a program to move the mouse.
Not a gap to be filled in silence: an X11 pointer is XWarpPointer, a Wayland one CANNOT be moved by a client at all, and a Windows one is SetCursorPos -- three different answers with three different rules about who may ask. On the platforms where the desk shows the screens a machine already has, rather than ones it made, the pointer is on them anyway.
var ErrNoScreens = errors.New("desk: no screens")
ErrNoScreens means a desk was asked for with nothing to show.
var ErrNoSuchApp = errors.New("desk: no such application")
ErrNoSuchApp says nothing on this machine answers to that name.
var ErrPointerLost = errors.New("desk: cannot read the pointer's position")
ErrPointerLost means the window server would not say where the pointer is.
It is not the same thing as ErrNoPointer off darwin, which says the pointer cannot be MOVED here: this one is a machine that can move it and would not say where it is.
var SelectionInk = toolkit.RGB(0x7B, 0xD8, 0x8F)
SelectionInk is the colour of the border round the chosen screen.
APPLE GREEN, asked for from the glasses: "instead of orange it would be gentler on the eye to have an apple green or a sky blue". Orange was chosen for being the one strong hue that is in almost no window chrome and in very little wallpaper — findable at a glance — and it is also the hue a headset pushes hardest at a person's eye across a whole session.
Green keeps what mattered and drops what did not. It is still nothing any window chrome uses, it is still unmistakable against six captured desktops, and it is not the theme's accent — which is a blue that has to sit politely beside the interface it belongs to, where this has to be FOUND.
Sky blue was the other suggestion and is one line away: RGB(0x7F, 0xC8, 0xF8). It is not the default only because it is a near neighbour of the accent, and two blues that mean different things is how a person stops trusting either.
var WaitingInk = toolkit.RGB(0xE0, 0x6C, 0x6C)
WaitingInk is the colour of the light when there is no desk.
RED, because the menu bar should answer the question without being opened, and "no light at all" is not an answer -- an icon with nothing on it reads as an icon, not as a state. Asked for: "on pourrait avoir un point rouge quand les lunettes ne sont pas en action".
Muted rather than a signal red: it is a resting state, not a fault, and a menu bar full of alarm colours teaches a person to stop looking.
Functions ¶
func Await ¶
Await waits for a display to show the desk on, and returns the one it found.
It exists because the glasses are a cable, and a cable is plugged in when a person gets round to it. The desk used to print "no display matches" and exit, which meant the order of two actions mattered for no reason: plug in, then start. Now it starts, says what it is waiting for, and begins by itself.
It returns AT ONCE when the display is already there, and logs nothing in that case: the normal path stays quiet.
While waiting it answers the menu bar — ErrAwaitQuit and ErrAwaitSettings — because the shortcuts are claimed by the desk's window, which does not exist yet. Nothing is created and nothing on the machine is changed until a display is found.
func BadgeDuration ¶
BadgeDuration turns a configured number of seconds into a duration, for a caller reporting what it will do.
func Billboards ¶
Billboards lists the USB Billboard devices on the bus -- the class a USB-C device presents when an alternate mode it supports could not be entered.
func ConfigPath ¶
ConfigPath reports the file the settings are read from.
That is $XRDESK_CONFIG when it is set, and otherwise desk.hcl under the platform's own configuration directory, beside the glasses catalogue:
~/Library/Application Support/go-xrkit/desk.hcl (macOS) ~/.config/go-xrkit/desk.hcl (Linux, or $XDG_CONFIG_HOME) %AppData%\go-xrkit\desk.hcl (Windows)
It fails only when the platform cannot say where a person's configuration lives, which on a Unix means HOME is unset.
func DarkNotePath ¶
DarkNotePath reports the file the notes are kept in.
func DisplayAt ¶
DisplayAt returns the display whose rectangle is exactly this one.
A rectangle identifies a display where a size does not: two identical monitors are the same size and are not in the same place. It is how the desk turns the screen it was given -- which it knows by name and geometry -- into the display id everything else here speaks in.
func DisplayOf ¶
DisplayOf returns the display an offer captures.
The identifier's shape — "display-N" — is the convention EVERY platform's Sources builds with, and it is derived from the display rather than handed out in order so that the same screen keeps the same identity across a restart. Reading it back is what lets a caller say "this position is showing one of the machine's own panels, and it is that one".
It answers false for anything else, a window among other things: there is no display behind a window's picture to speak of.
func DisplayOfferID ¶
DisplayOfferID is the Offer id for a display, for a caller that has just created one and wants it opened.
func EvidenceFor ¶
EvidenceFor returns the bus evidence that belongs to this display, and nil when nothing ties any of it to this display.
The bus says which headsets are ATTACHED. It does not say which display is which headset, and those are not the same claim. On the desk this was written at, a pair of XREAL One S enumerated over USB with no video link at all while a 7680x2160 monitor was the only display present: taking the model from the bus and the pixels from that monitor produced a plan for "XREAL 1S: 7 screens of 3840x2160", which is a headset's optics wrapped round somebody's desktop screen. A wrong field of view renders everything, in the wrong place, with no symptom — which is the whole reason the catalogue refuses to guess.
So evidence is applied only where the DISPLAY ties it to itself: it names a headset, and one on the bus is that model — or names a brand, and one on the bus is of that brand. Anything else gets nothing, including a display the person named by hand: with two headsets attached and nothing to tell them apart, the answer is no evidence rather than a coin toss, and with one it is still a guess at a name that would be printed as fact.
func FitScale ¶
FitScale is the largest magnification, at or below want, whose window still fits in maxW x maxH pixels. size reports the window's pixel size at a given scale.
Legibility and fitting are two different questions and they disagree. What makes the type comfortable on a 2160-row panel is a scale of three; what fits on the 1200-row display macOS may well put the window on is nothing like that. A window magnified past its screen is worse than a small one: the buttons are outside the frame, so the settings cannot be saved at all, and nothing on screen says why.
The measurement is handed in rather than computed here because the size depends on the FONT installed at that scale -- a row is a glyph high plus padding -- and installing a face is the display half's business. That also makes this the testable half: a synthetic size function proves the search without a window, a display or a font.
The step down is proportional, not a fixed decrement: if the window came out twice too tall, the useful next candidate is half the scale, not one notch less. It always shrinks -- the factor is a size over a smaller limit -- and it is bounded anyway, because this runs while a person waits for a window.
func Mirrors ¶
Mirrors picks, out of what the ribbon is showing, the machine's own panels that may be darkened.
Two things are left out, for two different reasons.
The displays this program MADE are not the machine's: there is no panel behind them and nothing to darken.
And any display the size of the one the desk itself is on is left alone. That is deliberately more than it needs to be: a ribbon position CAN be pointed at the glasses' own display — the sources list offers it like any other — and darkening that one would black out the very thing the viewer is looking at, with the key that undoes it now invisible. There is no reliable way here to turn the chosen screen's NAME into a display id, so the size is used, and the failure it can produce is the harmless one: a second panel of exactly the same size stays lit.
func OwnDisplay ¶
OwnDisplay is the display id of the screen the desk's window is on.
It is wiring: window.Screens says where a named screen IS, DisplayAt says which display that is, and both of those need a real window server in front of them. What is DONE with the answer -- never darkening that screen, and bringing a pointer that lands on it back onto the ribbon -- is decided in wrap.go and dim.go, where it can be tested.
func Peripherals ¶
Peripherals lists every headset on the USB bus.
Linux publishes what it enumerated under SysfsUSB, so this reads three files and opens no device node: no permission, and no chance of disturbing whatever holds the glasses.
func PhotoPath ¶
PhotoPath is where a photograph taken now may be written.
⛔ NEVER INSIDE A GIT WORK TREE, and that is not a convention -- it is the mistake this fleet has already made: a live test wrote a capture of a whole desktop into a public repository's testdata/, one `git add -A` from publication. A photograph from a headset is a picture of whatever the person wearing it was looking at, which is a stronger version of the same thing.
A .gitignore entry would be the wrong fix: ignoring is a safety net, not a barrier -- `git add -f`, a fresh clone, or any tool that does not consult it publishes the file anyway. go-fsctl/outdir is the barrier, and it REFUSES rather than choosing somewhere else, including when a person points XRDESK_PHOTO_DIR at their own work tree.
The name carries the moment, to the second, so two photographs taken in one session do not become one file.
func PutBackWhatWasLeftDark ¶
PutBackWhatWasLeftDark lights any panel a previous run darkened and never restored, and reports what it did.
It is called at start-up, before anything else touches a backlight. A panel that is ALREADY brighter than the note says is left alone: somebody has turned it up since, and taking that away would be this program overruling a person to tidy up after itself.
func RememberScreens ¶
RememberScreens writes the screen count into a settings file, leaving everything else in it exactly as it was.
⛔ It does NOT go through Config.Save. That renders the whole file from the struct with an empty hclwrite document, so everything the struct does not carry — every comment, every blank line somebody put between two blocks, the order they chose — is gone. That is defensible behind a Save button, where a person asked for their settings to be written down. It is not defensible on the side of an action that was about adding a screen: nobody clicking a tile in the gallery has agreed to have their file rewritten.
So the file is parsed and EDITED. hclwrite exists for exactly this: it keeps the tokens it did not touch.
A file that is not there yet is created holding only this, which is honest — it says the one thing that was decided and claims nothing else.
func Run ¶
Run shows the desk until the viewer quits.
Everything here is portable. The window, the toolkit and the warp all run on macOS, Linux, Windows and in a browser, so this loop is the same everywhere — only the screens and their pixels arrive by different roads.
func RunSettings ¶
func RunSettings(opt SettingsOptions) error
RunSettings shows the settings window and returns when it is closed.
It is a window of widgets and not a drawn picture: every control here is a toolkit widget, so it is navigable by keyboard, readable by an accessibility tree, and themed like everything else on the machine.
func Safely ¶
Safely runs fn and turns a panic into an error.
It exists because of what this program HOLDS while it runs, and what a panic does to that. The desk darkens the Mac's own panel while the ribbon shows a copy of it, and it puts the backlight back from a deferred call in the caller. A panic in the frame loop is a panic in ANOTHER GOROUTINE: it kills the process without running that deferred call, and somebody is left looking at a black screen with no menu bar to quit from.
That is not a hypothetical. It happened, with the glasses on:
panic: slice bounds out of range [:6684] with capacity 6680
and the report that came back was "I unplugged the glasses because I had lost access and there was no icon in the tray for me to stop the application" -- the icon was there, on a panel that had been turned off.
So a crash in the frame loop stops the desk the ordinary way instead: the run returns an error, every deferred restore runs, and the person gets their screen back. The stack is kept, because a crash that is swallowed silently is worse than one that is loud.
func Send ¶
Send puts an application's windows on a ribbon position.
The position is an index into ids, which is the desk's own screens in the order the plan made them — so position 1 in a settings file is screen 1 on the band, and a person never has to know what a CGDirectDisplayID is.
It returns what it moved and what it could not, rather than the first error: a desk of six applications where one is not running should place the other five and say which one it did not.
func SettingsScale ¶
SettingsScale is how much to magnify the settings window on a display of this height.
It is a LEGIBILITY choice and not a HiDPI derivation, which is worth being clear about: the back-end allocates one framebuffer pixel per logical point, so a widget tree is already the right size in the platform's own terms on every display. Nothing here has to compensate for anything.
What it compensates for is a person sitting in front of a very large panel. On the 8K screen this was reported against, the platform's own answer is a backing factor of one, so interface type comes out at its nominal size on a panel four times the area of a laptop's. A modest magnification reads better there; a proportional one does not -- scaling by pixel height gave a factor of three, and a dialogue magnified three times is a poster.
So: three steps, and the largest is a third again -- reported as still a little too big at half again, on the panel it was measured on. FitScale shrinks whatever this asks for until the window fits the display, so the worst case of getting this wrong is a window that is smaller than it might have been.
func ShouldChoose ¶
ShouldChoose reports whether to put the settings window in front of somebody before starting.
Only when nobody has decided and the machine cannot: several headsets on the bus, no model in the settings, and no display named on the command line. Naming one with -screen is the answer for a script, and writing one in the settings is the answer for a person who has already chosen — this is for the case where neither has happened and picking one would be a guess.
func TakePhoto ¶
TakePhoto opens a camera, waits for a picture, writes it and reports where.
⛔ IT OPENS AND CLOSES AROUND ONE PHOTOGRAPH. A camera held open is a camera left ON -- the light with it -- and a desk that kept one for the length of a session would be a headset that watches the room all afternoon so that a key press can be quick. Opening costs a second; that is the right price.
camera is a avfoundation.Camera.ID, or empty for the first the machine lists. On a headset with several, which is the case this is written for, the caller chooses.
func TrayIcon ¶
TrayIcon renders the menu-bar icon, as PNG bytes, with the light lit for the state given: green while a desk is up, red while there is none.
The SYSTEM's own symbol where there is one, and the toolkit's glasses otherwise. It is not a matter of taste: measured at 44 pixels, the toolkit's outline inks 7% of the box and a system symbol about 62%, and the difference is whether a person finds it among twenty other icons.
THE GLYPH IS RECOLOURED HERE because the platform has stopped doing it. An image carrying a colour is not a template, so macOS draws its pixels as they are -- and a system symbol is pure black, which on a dark menu bar sits among neighbours the platform has painted white at 85%. So the recolouring the platform would have done is done with the colour the platform would have used, read from the system when the icon is built rather than remembered: labelColor follows the appearance, and an icon built once would be wrong after somebody switches to dark.
func WritePhoto ¶
WritePhoto writes one picture where PhotoPath says it may go, and reports the path so a person can be told where to look.
⛔ IT SAYS WHERE. A photograph a program took and did not name is a photograph nobody can find, and the whole point of writing it somewhere durable rather than a temporary directory is that somebody comes back to it later.
Types ¶
type Action ¶
type Action int
Action is something the viewer asked for.
const ( // ActionNone is a key that means nothing here. ActionNone Action = iota // ActionNext and ActionPrev turn the ribbon by one screen, the short way // round. ActionNext ActionPrev // ActionFullscreen promotes the focused screen to fill the view, or puts it // back. ActionFullscreen // ActionGallery opens the gallery — every screen at once, as a grid — or // closes it again leaving the ribbon exactly as it was. ActionGallery // ActionGalleryOpen opens the gallery and ActionGalleryClose leaves it, // each doing nothing when the gallery is already in that state. // // They exist alongside the toggle because a system-wide shortcut is pressed // blind: the viewer cannot see whether the gallery is open before deciding, // and one key that means "open" from outside and "close" from inside is one // key that does the wrong thing whenever they have lost track. Two keys // always mean what they say. ActionGalleryOpen ActionGalleryClose // ActionChoose takes the selected screen and returns to the ribbon focused // on it. It means nothing outside the gallery. ActionChoose // ActionUp and ActionDown move the selection in the gallery. On the ribbon // they mean nothing: a band has no rows. ActionUp ActionDown // ActionCycle asks for the next source on the focused screen. What that // means is the application's business — this package knows the ribbon, not // what a platform has to offer — so it is reported through OnCycle rather // than acted on here. ActionCycle // ActionQuit ends the session. ActionQuit // ActionSettings asks for the settings window. // // It ENDS the ribbon, like ActionQuit, and says so through // [Desk.WantsSettings] -- because the two windows cannot be on screen at // once. A back-end holds one window: the ribbon covers a display entirely // and takes the keyboard, and a settings window opened behind it would be a // dialogue nobody can see waiting for an answer nobody can give. // // So the desk stops, the settings are changed, and the desk starts again on // them. Which is also the only order in which a changed setting can take // effect: the screen count, the headset and the shortcuts are all read on the // way in. ActionSettings // ActionCloser and ActionFurther move the band towards the viewer and away // from them, by [DistanceStep]. // // Further away is what shows the screens EITHER SIDE of the one in front: // they do not move, they take up less of the view, which is what pulling a // monitor back does. Nearer stops at one screen filling the view exactly, // because closer than that means seeing part of a screen. ActionCloser ActionFurther // ActionFit puts the band back where ONE screen fills the view exactly: // the largest a screen can be shown in these glasses, at one source pixel // per output pixel. // // It is [MinDistance], not a computed best. The near end of the range is // already defined as "one screen filling the view exactly, because closer // than that means seeing part of a screen" -- so the best fit is not // something to search for, it is the end of the scale, and this is the key // that returns to it without pressing closer six times. // // It is not "all the screens at once". That is what the gallery is for, and // [MaxDistance] exists to refuse it: at four screens across the view a // 1920-pixel screen is 480 pixels of a 1920-pixel view, which is a texture // rather than words. ActionFit // ActionFlatter and ActionRounder change the angle between one screen and // the next, by [SplayStep]. // // Flatter ends at one plane -- every screen square on, which is the band this // package drew before there was an angle and still what somebody wanting a // single wide surface should get. Rounder turns each screen further towards // the viewer, the way the two beside the middle one on a desk of three // monitors are turned. ActionFlatter ActionRounder // ActionPoint asks for the mouse pointer to be brought to the screen in // front of the viewer. // // It is the answer to the thing that made the desk unusable: the picture // shows screens that applications are running on, and the pointer is // somewhere else on the desktop -- reachable only by dragging it blind across // displays whose contents are captures of somewhere else. One key ends that. // // Moving a pointer is the platform's business, not this package's, so it is // reported through OnPoint rather than acted on here -- the same seam as // ActionCycle and for the same reason. ActionPoint // ActionApps opens the gallery of running APPLICATIONS, or closes it. // // It is the other gallery. The screen gallery answers "which desktop am I // looking at"; this one answers "what is open, and where is it" — a grid of // the applications that have a window, each saying which screen it is on. // // From it, ActionChoose puts the highlighted application on the screen the // band is showing. That pairing is deliberate: a system-wide shortcut is // pressed blind, and "put this where I am looking" needs no second key and // no number typed at a picture the person may not be able to see. ActionApps // ActionSpread hands out one screen per application, in order, up to the // ribbon's screen count. // // One key, and a desk of six empty desktops is a desk of six applications. // Applications past the last screen are LEFT WHERE THEY ARE rather than // wrapped onto a screen that already has one: two windows on one screen // hides one of them, and a person who pressed one key cannot be expected to // guess which. ActionSpread // ActionRemove takes the selected screen off the band, in the gallery. // // The gallery is where a person looks at the desk they have, so it is where // they will want one fewer as well as one more -- and the alternative is a // settings file, which is not where anybody is when they run out of use for // a screen. It means nothing outside the gallery and nothing on the cell // that ADDS one. ActionRemove // ActionAppsOpen shows the application gallery, and does nothing when it is // already up. // // It exists beside the toggle for the same reason ActionGalleryOpen does: a // system-wide shortcut is pressed BLIND. One key that means "show me what is // running" from outside and "put it away" from inside does the wrong thing // every time the person has lost track of which they are in. ActionAppsOpen // ActionStereo3D turns the 3D conversion on or off; ActionStereo3DOn and // ActionStereo3DOff say which, without asking what it is now. // // The same reasoning as the gallery: a system-wide shortcut is pressed // blind. Through the glasses the viewer can see whether the picture has // depth, so the toggle is honest there -- but a key that means "on" from a // laptop and "off" from the headset is a key that does the wrong thing // whenever they have lost track. // // It is not a navigation action, so it is answered before the ribbon, the // gallery and the application list are consulted: how the picture is SHOWN // has nothing to do with which screen is focused. ActionStereo3D ActionStereo3DOn ActionStereo3DOff // ActionScreen1 through ActionScreen9 go straight to that screen, the short // way round, instead of turning one at a time or opening the gallery to // pick. // // Nine, and not ten, because [MaxScreens] is nine: there is a digit key for // every screen a desk can carry and none spare. That is not a coincidence // worth hiding -- if the ceiling ever moves, this list is the second place // it has to move too, and a tenth screen with no key is a screen only the // gallery can reach. // // They are written out rather than derived from a base plus an offset. The // arithmetic would be shorter and would break the day somebody appends an // action in the middle of the run, silently, by turning "screen 4" into // something else. ActionScreen1 ActionScreen2 ActionScreen3 ActionScreen4 ActionScreen5 ActionScreen6 ActionScreen7 ActionScreen8 ActionScreen9 // ActionPhoto takes a photograph through one of the glasses' cameras. // // ⛔ IT IS A DELIBERATE ACT AND IT SAYS SO. A camera on a headset points at // whatever the person wearing it is looking at, so this is never // automatic, never on a timer, and never a side effect of anything else -- // and the desk puts the path it wrote on the picture, because a photograph // a program took and did not name is a photograph nobody can find. // // The light on the glasses is on for as long as the camera is open, which // is the hardware's doing and not something this could suppress if it // wanted to. ActionPhoto )
The actions a desk understands.
func KeyAction ¶
KeyAction maps a key to what it does.
These are the keys that work while the desk itself has focus. The shortcuts the viewer actually reaches for — turning the ribbon while working INSIDE one of the screens — cannot be these, because the focus is then in somebody else's application. Those are system-wide shortcuts and they are registered separately; this table is what remains useful when the desk is in front.
type App ¶
type App struct {
// Name is the application's own name, as the window server reports it.
Name string
// PID is the process it runs in, from the first window seen of it. Two
// processes with the same application name are one row in a gallery and one
// of these -- which is what a person means by "Firefox" whether or not it
// is two copies.
PID int32
// Icon is the application's own icon, or nil when nobody looked it up or
// the system would not give it. A gallery falls back to a drawn glyph.
Icon *Icon
// Windows is how many windows it has.
Windows int
// On is the ribbon positions its windows are on, ascending and without
// repeats. Empty means every window of it is somewhere else — on a real
// display, most likely the one the person is looking at.
On []int
// Minimized is how many of its windows are in the Dock. A minimized window
// can still be moved, so it is counted rather than hidden, but a gallery
// should say so: an application that is entirely minimized looks missing.
Minimized int
}
func AppsFrom ¶
func AppsFrom(list []accessibility.WindowInfo, ids []uint64) []App
AppsFrom groups a listing of windows into applications, and works out which ribbon position each window is on.
It is pure, which is the point: the listing comes from the platform once, and everything a person then chooses over is decided here, where it can be tested.
ids is the desk's screens in ribbon order — the same slice Send takes — so position 1 in a settings file, screen 1 on the band and "on screen 1" in a gallery are the same screen. A window on a display that is not the desk's counts towards the application's window total and towards no position.
The result is sorted by name, so a gallery does not reshuffle itself between two looks at it. Windows with no application name are ignored: there is nothing to show and nothing to move.
type AwaitOptions ¶
type AwaitOptions struct {
// Want is the display or headset asked for: a -screen name, or the model
// from the settings. Empty means "whatever is here", which never waits,
// because [glasses.ChooseDisplay] always finds something to use.
Want string
// List answers what is attached NOW. Required.
List func() ([]glasses.Display, error)
// Actions is the menu bar, so a person is not stuck with a program that
// only waits. Nil is allowed and means nothing can interrupt but the
// context.
Actions <-chan Action
// Every overrides [AwaitPoll].
Every time.Duration
// Asked says the person has ALREADY been asked which headset to use, and
// did not choose one.
//
// Without it a settings file naming a headset that is not here, and a
// headset here that nobody has chosen, is a loop: ask, no answer, ask
// again. Measured by the bench on an unattended machine -- the settings
// window opening over and over in one session's log -- and it would look
// the same to a person who closed it.
//
// Asked once, the answer is to get on with it: use the headset that is
// here and say so.
Asked bool
// Logf says what is happening. Nil is silence.
Logf func(string, ...any)
}
AwaitOptions is what Await needs.
type Bench ¶
type Bench interface {
// Displays lists what the window server is driving, virtual ones included.
Displays() ([]accessibility.Display, error)
// Windows lists the windows of every application whose name contains app,
// case-insensitively. A person types "safari", not "Safari".
Windows(app string) ([]accessibility.Window, []string, error)
// Trusted reports whether this process may move another one's windows.
Trusted() bool
// Listing is every window on the machine, each attributed to the display it
// is mostly on. It holds no handles open, which is what makes it usable for
// a gallery: the list is read, shown and chosen from, and only the choice
// opens anything.
//
// Applications that AX declines to describe do not appear. That is the
// window server's answer rather than a gap here — Activity Monitor answers
// AXError -25204 to any question about its windows on the machine this was
// written on.
Listing() ([]accessibility.WindowInfo, error)
}
A Bench is what putting an application on a screen needs of the platform.
It is an interface because the whole of the policy below — which windows an application has, which screen a position is, what to do when the application is not running or the screen is not there — is worth testing on a machine with no windows on it at all, which is every machine a test runs on.
func TheBench ¶
func TheBench() Bench
TheBench is the platform, for Send.
macOS is the only one wired up. Moving another application's window is the Accessibility API's job there; Linux has the same idea in each window manager's own protocol and Windows in SetWindowPos, and until those are written this answers that it cannot — which is a thing a person can be told, rather than a placement that silently does nothing.
type Canvas ¶
Canvas is the picture the screens are drawn into, and the window shows.
It used to be a panorama that a projection then read from. There is no projection any more: the screens are flat, so the buffer they are composited into IS what the glasses are given.
func (*Canvas) Blit ¶
Blit draws one screen into the canvas, where the ribbon said it goes.
It does not gather pixel by pixel. The horizontal mapping is a linear stepper whose step, for a panorama sized to show a screen at its own resolution, is very near one — measured at 0.99982 for a VITURE Beast. A step that near one means the source column advances by exactly one for long stretches, breaking only when the accumulated fraction wraps: over a thousand columns at a time.
So the columns are decomposed into RUNS of consecutive source pixels, once per blit, and each run is copied as a block. Every row of the rectangle reuses the same decomposition, because on a cylinder a screen's longitudes do not vary with height.
Gathering one pixel at a time cost 8.7 ms a frame. It was not the arithmetic: it was doing four-byte copies two and a half million times.
func (*Canvas) Compose ¶
Compose fills the canvas and draws every blit, which is one frame's worth of panorama. sources is indexed by ribbon screen number; a screen with no source yet — a display just created, a capture not started — is simply left as background rather than drawn as garbage.
func (*Canvas) ComposeSlants ¶
ComposeSlants is Canvas.Compose for turned screens: the background, then every panel in the frame.
Back to front is not a question here. A chain of panels hinged edge to edge cannot overlap in the view -- each one starts where the last one ended -- so the order they are drawn in cannot change the picture. That is a property of the arrangement, not of the loop, and it is why there is no depth sort in a renderer that draws a three-dimensional thing.
func (*Canvas) Fill ¶
Fill paints the whole canvas one colour, which is what the gaps between screens show.
func (*Canvas) Slant ¶
Slant draws one turned screen: a column at a time, each from its own source row range.
It is the slow path and it says so. Canvas.Blit copies whole runs of a row because every row of a rectangle reads the same columns; a trapezoid's columns each have their own height, so there is no run to copy and the pixels are gathered one at a time. That is the four-byte-copy cost the fast path exists to avoid, which is why the screen in FRONT of the viewer -- the one turned by nothing, and the only one whose pixels a person is reading -- goes through Blit and only its neighbours come here.
Rows outside, columns inside: the writes then run along the canvas in order, and it is the reads that jump. One of the two has to, and a sequential write is worth more than a sequential read on every machine this has been measured on.
type Closer ¶
type Closer interface{ Close() error }
Closer is what OpenTray returns: the item, to be taken out of the menu bar when the session ends.
It is io.Closer by shape and not by import, so the portable half of this file carries no dependency on the platform half.
type Config ¶
type Config struct {
// Shortcuts are the system-wide combinations, by action name.
Shortcuts []ConfigShortcut `hcl:"shortcut,block"`
// Fallback is what to add, in order, when a combination is already taken.
// Nil means [DefaultLadder]; an explicitly empty list means do not fall
// back at all, and report the refusal instead.
Fallback *[]string `hcl:"fallback"`
Ribbon *ConfigRibbon `hcl:"ribbon,block"`
Glasses *ConfigGlasses `hcl:"glasses,block"`
// Places are the applications to put on the band at start-up.
Places []ConfigPlace `hcl:"place,block"`
}
Config is everything a person may set without a Go toolchain.
HCL rather than JSON, for the two reasons a person gives when asked: it takes comments, so a file can say WHY a shortcut was moved, and it has a schema, so a typo is a diagnostic pointing at a line rather than a field silently left at its zero value.
func LoadConfig ¶
LoadConfig reads ConfigPath. A file that is not there is not an error: it means every default stands.
func LoadConfigFile ¶
LoadConfigFile reads one settings file.
func (Config) BadgeSeconds ¶
BadgeSeconds is how long the screen's number stays up after the band moves. A file that does not say asks for DefaultBadgeSeconds; zero turns it off.
func (Config) Bytes ¶
Bytes renders the settings as the file they came from.
Written with hclwrite rather than by printing strings, so that what comes out is HCL by construction: a quotation mark in an application's name, or a name with a newline in it, cannot produce a file that then fails to parse. The round trip — write, read back, compare — is the test.
What is NOT set is left out entirely rather than written as a zero. A file with no `screens` line means "as many as the default says", and a file with `screens = 0` means the same thing while looking like a decision; only one of those is honest about what the person chose.
func (Config) Distance ¶
Distance is how far the band sits from the viewer, or 1 when the settings do not say. See Plan.Distance.
func (Config) HotkeyOptions ¶
HotkeyOptions is what ClaimGlobal should be given for this configuration.
⛔ OnThisKeyboard, ALWAYS. A key name in a settings file is the LEGEND a person reads off the key in front of them, and a hotkey.Key is a virtual key code -- a POSITION, named after the ANSI legend for that position. On a French Mac the position called Equal prints "-", and "=" is over on the position called Slash. So `shortcut "fit" { keys = "ctrl+alt+cmd+Equal" }` claimed the key printed "-": the shortcut was granted, it fired, and pressing the key printed "=" reached nothing at all. Every check said it was granted, because it was. "le raccourci du fit ne fonctionne pas."
A desk shortcut is always the legend and never the position: there is no shape-under-the-hand here, only keys a person is told about in a menu.
func (Config) Immersive ¶
Immersive reports whether to cover the glasses display's own menu bar and Dock. A file that does not say asks for it.
func (Config) Ladder ¶
Ladder returns the fallback ladder this configuration asks for.
A file with no `fallback` gets DefaultLadder. A file with `fallback = []` gets no fallback at all, which is a real thing to want: it says "give me the combination I asked for or tell me you could not".
func (Config) Mirror ¶
Mirror says whether the first ribbon position shows this Mac's own screen. A file that does not say asks for it.
func (Config) Model ¶
Model is the headset this configuration prefers when several are attached, or "" when it does not care.
func (Config) Placements ¶
Placements are the applications to put on the band, in the order written.
func (Config) Save ¶
Save writes the settings to ConfigPath, creating the directory if it is not there.
It writes a new file and renames it over the old one. A settings file half written is a settings file that will not parse, and the next start-up would then refuse to run over a power cut in the middle of a save.
func (Config) ShortcutsOr ¶
ShortcutsOr returns the configured shortcuts, falling back to base for any action the file does not mention.
A settings file that names one shortcut must not silently drop the other two: a person moving the gallery key is not asking to lose the arrows.
func (Config) SplayDeg ¶
SplayDeg is the angle between one screen and the next, or DefaultSplayDeg when the settings do not say. See Plan.SplayDeg.
A zero in the file is honoured: it means the flat band, which is a real choice and not an absence. Nil is the absence, and that is what gets the default.
type ConfigGlasses ¶
type ConfigGlasses struct {
// Model names which headset to use when several are attached, as the
// catalogue names it.
Model *string `hcl:"model"`
}
ConfigGlasses is the `glasses { }` block.
type ConfigPlace ¶
ConfigPlace is one `place "Safari" { screen = 2 }` block.
type ConfigRibbon ¶
type ConfigRibbon struct {
// Screens is how many virtual screens to put on the band. Zero asks for
// [DefaultScreens].
Screens *int `hcl:"screens"`
// Splay is the angle between one screen and the next, in degrees. Nil means
// [DefaultSplayDeg]; zero is the flat band. See [Plan.SplayDeg].
Splay *float64 `hcl:"splay"`
// Distance is how far the band sits from the viewer, as a multiple of the
// distance at which one screen fills the view. Nil, or anything below one,
// means one. See [Plan.Distance].
//
// It is here as well as on the keyboard because it is a PREFERENCE, not a
// gesture: somebody who works with three screens in view wants three screens
// in view every session, and setting it twelve times a day with a shortcut is
// not a setting, it is a chore.
Distance *float64 `hcl:"distance"`
// BadgeSeconds is how long the screen's number stays up after the band
// moves. Nil means [DefaultBadgeSeconds]; zero turns it off.
BadgeSeconds *float64 `hcl:"badge_seconds"`
// Mirror reserves the FIRST ribbon position for this Mac's own screen, and
// makes one virtual display fewer. Nil means true.
//
// Somebody wearing the glasses still has a Mac in front of them, with a menu
// bar, a Dock and whatever was already open on it. Reaching it should not
// mean taking the glasses off, and it should not mean knowing a key: it is
// screen 1, where the band starts.
Mirror *bool `hcl:"mirror"`
// Immersive covers the glasses display's own menu bar and Dock. Nil means
// true.
//
// macOS draws a menu bar on EVERY display when Spaces are separate, so the
// glasses carry one of their own — and being drawn at a level above an
// ordinary window, it sits on top of the picture. Covering it is the whole
// point: the desktop being shown has a menu bar of its own already, and two
// of them was the first thing anyone noticed wearing this.
//
// Turn it off if the glasses are the MAIN display, where that bar and the
// Dock are the real ones rather than a copy on a screen nobody is using.
Immersive *bool `hcl:"immersive"`
}
ConfigRibbon is the `ribbon { }` block.
type ConfigShortcut ¶
ConfigShortcut is one `shortcut "next" { keys = "..." }` block.
type Desk ¶
type Desk struct {
// Background is what the gaps between screens show.
Background [4]byte
// OnAdd, when set, is what the gallery's "add a screen" cell calls. It must
// return a feed for the new screen — which on macOS means creating another
// virtual display and capturing it.
//
// It is a callback and not something this package does, for the same reason
// the feeds are handed in rather than opened here: making a display is the
// platform's business and this file has no operating system in it.
OnAdd func() (Feed, error)
// OnCycle, when set, is called with the FOCUSED position when the viewer
// asks for the next source there. It is called without the desk's lock held,
// so a handler may call SetFeed — which is the whole point of it.
OnCycle func(pos int)
// OnStereo3D, when set, is told whenever the 3D conversion is turned on or
// off. Like OnCycle it is called OUTSIDE the lock: whoever answers it opens
// a depth model and talks to a GPU, which must not happen while the ribbon
// is held.
OnStereo3D func(on bool)
// OnPoint, when set, is called with the position whose screen the pointer
// should be brought to. It is called without the desk's lock held.
//
// The desk knows which screen a person is looking at; only the application
// knows what a screen IS to the window server. So this is the seam, like
// OnCycle -- and it is the one that makes the desk usable rather than merely
// visible: without it the pointer has to be dragged blind across displays
// whose contents are captures of somewhere else.
OnPoint func(pos int)
// OnRemove, when set, is called after a screen has been taken off the band,
// with the position it was at and the feed that was on it.
//
// The desk has already shrunk by then: what is left is the platform work --
// closing the capture and giving the display back -- which this package does
// not do, for the same reason it does not create one.
OnRemove func(pos int, f Feed)
// OnScreens, when set, is called after the number of screens on the band
// has changed by a viewer's doing -- the gallery's "+" tile, or taking one
// away -- with the count there is now.
//
// It is the seam for REMEMBERING that: this package does not know where
// settings live, and a caller that does can write the number down so the
// desk comes back the same size. See [RememberScreens], which edits the
// file rather than rewriting it.
//
// It does not fire from [Desk.Grow] or [Desk.Shrink] called directly, the
// same way [Desk.OnRemove] does not: a program that called those already
// knows what it did. This is for the paths a PERSON drives.
OnScreens func(count int)
// OnApps, when set, answers what is running whenever the application
// gallery is opened. It is asked EVERY time rather than once, because the
// list is what a person opened the gallery to see: an application that
// quit two minutes ago must not still be offered a screen.
//
// A callback for the same reason as the others: enumerating windows talks to
// the window server, and there is no operating system in this file.
OnApps func() ([]App, error)
// OnPlace, when set, is handed the placements the viewer asked for — one
// from the application gallery, or a screen each from [ActionSpread].
//
// It is called without the desk's lock held, and it deliberately hands over
// [Placement]s rather than doing anything: the live path is then the same
// one the settings file uses at start-up, with the same menu-bar allowance
// and the same reporting, instead of a second placement path that would
// drift from it.
OnPlace func(places []Placement)
// OnPhoto takes a photograph and reports where it was written.
//
// It is a SEAM because a camera is the platform's: this package decides
// when a photograph is taken and what is said about it, and the application
// decides which camera and how. Nil is a desk with no camera, and asking for
// a photograph then says so rather than doing nothing.
OnPhoto func() (string, error)
// contains filtered or unexported fields
}
Desk is the running arrangement: a plan, the screens on their ribbon, the feeds filling them, and the panorama they are drawn into.
Everything here is portable. It knows nothing about how a screen was created or how its pixels arrive — only that a Feed will hand them over. That is what lets the same logic run over ScreenCaptureKit, X11 and Windows, and what lets it be tested against feeds that are not screens at all.
func New ¶
New builds a desk from a plan and one feed per screen.
The feeds are taken in the plan's own order, so feeds[i] fills the screen the plan calls screen-(i+1). A nil feed is allowed and simply shows background: a display that exists but is not being captured yet is a normal state during start-up, not an error.
func (*Desk) Badge ¶
Advance moves the ribbon towards where it is going, dt seconds later. Badge turns the arrival badge on for this many seconds, or off at zero. It must be set before the first Render.
func (*Desk) Click ¶
Click chooses a screen from the gallery, at a point in the picture's own coordinates. It reports whether the click landed on a screen.
One click goes there, rather than one to highlight and another to confirm. The gallery exists to be left: somebody who has found the screen they want has already decided, and asking them to say so twice is asking them to aim twice at a tile in a headset.
On the band it does nothing. A click on a captured desktop is not this application's to interpret — the desktop under the cursor will have its own idea of what was clicked.
func (*Desk) Distance ¶
Distance is how far the band is from the viewer. See Plan.Distance.
func (*Desk) Err ¶
Err reports what the last action returned, or nil. The gallery can refuse — a direction it has no cell for — and a refusal nobody can see is a key that silently does nothing.
func (*Desk) Focus ¶
Focus is the ribbon position in front of the viewer.
It is what a caller needs to put something THERE -- the screen a wandering pointer went to, most often -- without having to track every action that could have turned the band since it last looked.
func (*Desk) Grow ¶
Grow puts another screen on the band, at the end, and returns its position.
Everything that was sized for the old count is rebuilt: the placement, the band, the gallery. Rebuilt rather than grown, because the layout is DERIVED from the count — the pitch between screens is a share of the whole turn — so there is no version of this that adjusts one number and leaves the rest standing.
The viewer keeps the screen they were facing. Adding a seventh screen must not move the band, or a person who added one to put something on it would find themselves somewhere else.
func (*Desk) InGallery ¶
InGallery reports whether a gallery covers the view -- either of them.
It is what tells a caller when the BARE keys are worth claiming: while a gallery is up the person is looking at the desk rather than working in an application, and the arrows should mean what they look like they mean. See GalleryShortcuts.
func (*Desk) Look ¶
Look turns the band to a screen without a key being pressed.
It is what lets the ribbon FOLLOW something — the pointer, today. A position it has no screen for is refused; the position it is already on is nothing, deliberately, because "follow" is asked several times a second and a band that re-aimed at the screen it is already on would never settle.
It is not a shortcut for Desk.Do: an action is what a person asked for, and this is the desk keeping up with them.
func (*Desk) Render ¶
Render draws the current frame into the panorama and returns it.
Every feed is asked for its latest pixels whether or not it is visible: a feed that is never read may stop delivering, and the cost is a pointer.
func (*Desk) SetFeed ¶
SetFeed replaces what one ribbon position shows, while the desk is running.
It returns the feed that was there, and does NOT close it. That is deliberate: a viewer who parks a screen to look at something else usually wants it back, and a method that closed it would make "swap" and "discard" the same gesture. A caller that really is finished with the old feed closes it itself.
The last picture from the old feed is dropped with it, so a position whose new feed has not produced anything yet shows background rather than the previous screen's contents — which would be a lie about what is on that panel.
func (*Desk) Shrink ¶
Shrink takes screen pos off the band and returns the feed that was on it.
The feed is RETURNED rather than closed, exactly as Desk.SetFeed does: what a caller wants to do with a capture it is no longer showing is the caller's business, and a method that closed it would make "put it away" and "throw it away" the same gesture.
It refuses to take the last screen. A desk of nothing is not a desk, and everything here — the ribbon, the gallery, the navigator — is built from a plan of at least one; the person who wants none wants to quit.
func (*Desk) WantsSettings ¶
WantsSettings reports whether the desk stopped in order to show the settings rather than to end the session.
It is a separate question from Desk.Quit because both are true at once: the ribbon has to come down either way, and only the caller knows what to put in its place.
type Dimmer ¶
type Dimmer struct {
// contains filtered or unexported fields
}
Dimmer keeps the machine's own panels dark while a copy of them is on the ribbon.
It exists because of what mirroring MEANS. A person wearing display glasses that show their Mac's screen is looking at a copy; the panel itself is then a second, brighter copy of private work at reading distance, facing whoever walks past, lit at full power for nobody.
Turning the backlight off is not the same as covering the screen with a black window. The framebuffer is untouched, so the picture ON THE RIBBON does not change — there is no window for the capture to exclude, no stream to rebuild, and nothing another program can raise itself above.
The way home is read before anything is changed and kept here, so [Restore] can always be deferred. A program that darkens somebody's screen and then dies leaves them with a black panel and no idea why.
func (*Dimmer) Note ¶
note records what is dark, or removes the note when nothing is. Note writes down which panels are dark and what they were lit at, or removes the note when none are.
Called after every change, because the point of it is to be there when this process is not.
func (*Dimmer) Restore ¶
Restore puts every panel back the way it was found. It is safe to call twice, which is what lets it be deferred beside a Showing that runs many times.
func (*Dimmer) Showing ¶
Showing tells the dimmer which of the machine's own displays are on the ribbon now. Panels that have just arrived are darkened; panels that have left are put back.
It is the whole API on purpose: the caller says what the world looks like and the dimmer works out the difference, rather than the caller having to remember what it darkened and pair every change with its undo.
type Fan ¶
type Fan struct {
// contains filtered or unexported fields
}
A Fan is the band with its screens TURNED: a chain of flat panels hinged edge to edge, each angled by the splay from the last, projected for wherever the viewer is looking.
It is the second half of being able to push the band back. Strip draws the band with every screen square on, which is what it looks like from directly in front and wrong for the ones off to the side: at a desk of three monitors the two beside the middle one are turned towards you. A Fan turns them.
The chain is INFINITE and the screen index wraps, which is how the band stays a ring: panel n is screen 0 again, one turn further along the chain. So walking right past the last screen arrives at the first, exactly as it did flat, and nothing about the navigator or the gallery had to learn what an angle is.
The screens are still FLAT. A curve bows the screen you are reading, which argues with the depth the glasses already present -- that was measured, worn, and deleted. A rotation leaves every panel flat and only changes which way it faces. See Slant for what one turned panel projects to, and why that is a trapezoid rather than a guess.
func NewFan ¶
NewFan prepares the chain for this plan.
It refuses a plan with no screens or no size, and a splay of nothing -- which is not a fan at all but the flat band, and Strip draws that better: every panel square on means every panel is a rectangle, and a rectangle is a run of row copies rather than a pixel at a time.
func (*Fan) Angle ¶
Angle is where panel j of the chain is, in radians from straight ahead.
It is the angle to the panel's CENTRE, which is what the scroll interpolates between: the panels of a chain do not subtend equal angles -- the ones further along are further away -- so a position half way between two screens is half way between their two angles and not half of a fixed pitch.
func (*Fan) Frame ¶
Frame appends the panels in shot to dst: the focused screen and its neighbours, for a band that has moved `toward` screens past the focused one (see Strip.Toward).
The columns of the returned slants live in the Fan's own buffers, so every slant of one frame may be held at once (the drawing loop does) and the frame after this one overwrites them.
func (*Fan) SetSourceWidths ¶
SetSourceWidths gives screens their own source widths.
It is not decoration: the columns of a turned panel are gathered one pixel at a time out of the source row, so a panel told it is 1920 pixels wide when its capture is 1670 reads past the end of the row. That is a PANIC, and it is how this was found -- "slice bounds out of range [:6684] with capacity 6680", where 6680 is 1670 pixels of BGRA, with the app running and the glasses on.
A nil or short slice, or an entry of zero, leaves that screen on the width every other screen has.
type Feed ¶
type Feed interface {
// Frame returns the latest pixels, and whether they are newer than the last
// call. A feed with nothing yet returns a zero Source and false.
Frame() (Source, bool)
// Close releases the capture. It must be safe to call twice.
Close() error
}
Feed is one screen's pixels, arriving over time.
It is an interface rather than a concrete capture handle because what is behind it differs completely per platform — ScreenCaptureKit, X11 shared memory, a Windows duplication texture — while what this package needs of it is the same everywhere: the most recent pixels, and whether they are new.
Frame must NOT allocate and must NOT copy: it hands back a borrowed view of whatever the capture already has. The whole frame budget is 16.6 ms and a capture that copies a 4K screen spends a fifth of it doing nothing useful.
type Fence ¶
type Fence struct {
// contains filtered or unexported fields
}
Fence keeps the pointer on the screen the band is showing.
THE MOUSE DOES NOT CHANGE SCREENS. The keyboard does.
It was the other way round and it did not work. The band followed the pointer, the pointer came back at the other end of the band when it was pushed off it, and a display nothing was showing was fetched onto the screen in front of the viewer when the pointer wandered onto it. Three mechanisms, each answering a hole left by the one before, and the report after every one of them was the same: "I have lost the mouse again".
The reason is simple enough once it is said. A person wearing display glasses sees ONE screen. The desktop under the pointer is several, most of them invisible, and a pointer that can leave the visible one is a pointer that can be somewhere its owner cannot look. No amount of following fixes that; it only decides how far away the mouse gets before something notices.
So the pointer stays. It is put back on the screen the band is showing, every frame, and the way to another screen is the key that turns the band -- which also brings the pointer, because the screen it is held to has changed.
func (*Fence) Step ¶
Step puts the pointer back on the screen at position focus, if it has left it. It reports whether it moved it.
showing is the display each ribbon position is showing, in order. A position showing nothing, or a display this machine will not measure, is not a fence: the pointer is left alone rather than held against a rectangle nobody knows.
type Gestures ¶
type Gestures struct {
// contains filtered or unexported fields
}
Gestures turns three-finger swipes on the trackpad into ribbon actions.
It exists because the keyboard is the only way to turn the ribbon, and a person wearing the glasses has a trackpad under their hand. The mouse is deliberately NOT that way in: the pointer used to change screens and it kept getting lost, which is why it no longer does. A GESTURE is not the pointer -- it moves nothing on screen and cannot wander onto a display nobody is looking at.
It asks for no permission. macOS delivers a three-finger swipe as a private event the Dock consumes first, and only when the person has bound the gesture to switching spaces; reading the contacts underneath depends on neither. See go-macos/multitouch.
func ClaimGestures ¶
func ClaimGestures() *Gestures
ClaimGestures starts watching the trackpad. It never fails: a machine with no trackpad, or one where the framework will not load, yields a Gestures whose channel simply stays empty -- the desk is driven by the keyboard, and the gesture is an addition to it.
type Grid ¶
type Grid struct {
// contains filtered or unexported fields
}
A Grid is every screen at once, in front of the viewer.
It is the flat counterpart of the ribbon's gallery, and it is head-locked on purpose: the band may be anywhere, but the grid is always straight ahead, so opening it and closing it costs no motion and shows the ribbon exactly as it was left.
Left and right WRAP, because the band is a circle and the last screen really is next to the first. Up and down CLAMP, because the fold into rows is this type's invention and has no seam to walk through.
func NewGrid ¶
NewGrid folds n screens of srcW x srcH into a view of viewW x viewH, in DefaultColumns where that holds them and in whatever leaves them biggest otherwise.
It lays out ONE MORE cell than there are screens: the last one is where a screen is added. A gallery is where somebody looks at the desk they have, so it is where they will want another one — and the alternative is a settings file, which is not where anybody is when they run out of room.
func NewGridCols ¶
NewGridCols folds the screens into exactly cols columns. A cols of zero or less asks for the shape that leaves the screens biggest, and so does a cols that turns out to leave no cell at all: a person asking for six columns in a window too narrow for them should get a gallery, not a refusal.
func (*Grid) Adder ¶
Adder is the index of the cell that adds a screen, and false when this grid has none.
func (*Grid) At ¶
At is the screen whose cell contains the point, in view coordinates.
It is how a mouse chooses a screen. The gaps between cells and the margin round the grid belong to nothing: a click there is a click on the background, which is a click that should do nothing rather than the nearest thing.
func (*Grid) Cell ¶
Cell is where screen i sits in the view, for a caller drawing a highlight round the selected one without asking this type for pixels.
func (*Grid) Frame ¶
Frame appends the blits for the whole grid and returns the extended slice. There is no offset argument, and that is the point: the grid is head-locked.
func (*Grid) Len ¶
Len is how many screens the grid holds. It is what makes a Grid a ribbon.Selectable, so the navigator drives it without knowing that these screens are flat. Len is how many SCREENS this gallery is of, which is what a navigator needs of it — not how many cells it draws. The cell that adds a screen is this type's own furniture and is none of the navigator's business.
func (*Grid) Move ¶
Move walks the selection. Left and right wrap; up and down stay in their column, and do nothing when it has no cell that way.
func (*Grid) SetSourceWidths ¶
SetSourceWidths gives cells their own source widths, for screens that are not the shape of the band.
The cells stay the same size -- a gallery whose tiles were different widths would be a grid nobody could scan -- so a narrower screen is drawn NARROWER INSIDE its cell, centred, rather than stretched across it. Stretching would be worse than the empty band it replaces: proportions a person recognises are how they tell one desktop from another at a glance.
A nil or short slice, or an entry of zero, leaves that cell as it was.
type Hotkeys ¶
type Hotkeys struct {
// contains filtered or unexported fields
}
Hotkeys is a set of claimed global shortcuts, feeding one channel of actions.
func ClaimGallery ¶
func ClaimGallery() *Hotkeys
ClaimGallery claims GalleryShortcuts with no fallback ladder.
No ladder on purpose: a bare arrow that could not be claimed must stay unclaimed rather than becoming ⇧← , which is a selection in every text field on the machine.
func ClaimGlobal ¶
ClaimGlobal claims each shortcut, substituting when it has to.
It returns a usable set even when nothing could be claimed: a platform with no global shortcuts, or a desktop where every candidate is spoken for, is a reason to run without them, not a reason not to run. What went unclaimed is in Hotkeys.Describe, for the application to show.
func (*Hotkeys) C ¶
C is the actions the claimed shortcuts produce. It closes when every claim has been released.
func (*Hotkeys) Describe ¶
Describe says what was claimed and what was not, in the form a person needs to see rather than the form a log wants.
The claimed combination is SHOWN, not merely recorded. One of the three kinds of conflict — an application's own menu key — cannot be detected by anything, so a shortcut that registers without complaint may still be one another application was quietly using. Naming what was taken is what lets the viewer notice.
func (*Hotkeys) DescribeNames ¶
DescribeNames is the same, spelled out: "Option-Command-Left" rather than "⌥⌘←".
The glyphs are what macOS prints on a menu and what a terminal shows perfectly. They are NOT in every font — rendered in a window with the toolkit's own, ⌥ ⌘ ⇧ ⌃ ← → all came out as nothing at all, so a line meant to say which combination was granted said "previous:" and stopped. Anywhere the font is not known, this is the one to use.
func (*Hotkeys) Granted ¶
Granted is the combination each action ended up with, as a menu prints it.
What was CLAIMED, not what was asked for. The ladder substitutes when a combination is taken, so the two differ exactly when it matters most -- a menu that printed the wanted one would send a person to press a key that does nothing, and the substitution is the one case where nobody can guess.
An action nothing was granted for is absent rather than empty, so a caller can tell "no combination" from "a combination that renders as nothing".
The COMBINATION and not a rendering of it: a menu row draws its own, and on macOS it draws it right-aligned in a column with every other row's, in the system's own glyphs. Handing over a string would have decided that for it.
type Icon ¶
An App is a running application with at least one window, and where its windows are on this desk.
It is a description, like Offer: it holds no handle on anything, so a gallery of applications can be built, shown and chosen from without opening a window of any kind — and tested on a machine with none. An Icon is an application's own icon, as straight RGBA, W*H*4 bytes.
It is pixels rather than a path or a handle because this package has no operating system in it: whoever fills it in knows how to ask -- on macOS that is go-macos/appicon, which needs no permission -- and everything here just draws it.
type Inventory ¶
type Inventory struct {
// contains filtered or unexported fields
}
Inventory is what CAN be shown, and what currently is.
It holds no pixels and opens nothing. It exists so that choosing what a screen shows is a decision a person makes over a list they can read, separately from the machinery that then makes it appear — and so that the choosing can be tested without a display anywhere in sight.
A source may be on at most ONE position at a time. Two positions showing the same display would each cost a capture of the same pixels, and a viewer turning the ribbon would pass the same screen twice and wonder which is real.
func NewInventory ¶
NewInventory describes a ribbon of the given length and what may go on it.
Offers with a duplicate or empty ID are refused rather than silently collapsed: an identifier that names two things cannot be used to choose between them.
func (*Inventory) Assign ¶
Assign puts a source on a position, replacing whatever was there.
It refuses a source that is already elsewhere on the ribbon rather than moving it, because a viewer who meant to move it and a viewer who forgot where it was make the same gesture, and only one of them wants the other position emptied. Clear the other position first to say which was meant.
func (*Inventory) Clear ¶
Clear empties a position. Clearing an empty one is not an error: a viewer pressing the same key twice meant it both times.
func (*Inventory) Cycle ¶
Cycle moves a position to the next source that is not already shown, and to empty after the last one.
It is the whole of "choose what this screen shows" reduced to one key. A picker that lists everything is better once there is somewhere to draw it; until then this makes the choice reachable with the keyboard alone, and it stays useful afterwards for the viewer who just wants the next thing.
The second return is false when the position ended up empty.
type Kind ¶
type Kind int
Kind is what sort of thing a source is.
const ( // KindDisplay is a display whose pixels are captured — a real monitor, or // one this program created. KindDisplay Kind = iota // KindPanel is a surface this program renders onto and reads back. On // Android that is a virtual display carrying a WebView or a decoder: things // the platform can draw and a CGO-free Go process cannot. KindPanel )
The kinds of source a ribbon position can hold.
type Offer ¶
type Offer struct {
// ID identifies this source within an inventory, and is stable enough to
// survive being written down and used later.
ID string
// Name is what a person should be shown.
Name string
// Kind is what sort of thing it is.
Kind Kind
// W and H are its pixel size, or zero when it has none yet — a panel's size
// is decided when it is opened.
W, H int
// Main reports the machine's own primary screen: the one carrying the menu
// bar, which is what somebody means by "my Mac's screen".
//
// A field rather than a word in the name. The name is for a person to read
// and a caller that looked for "(main)" in it would be reading a sentence
// back out of a label -- and would stop working the day the label changed,
// silently.
Main bool
}
Offer is something that can be put on a ribbon position.
It is a description, not a handle: an inventory can be shown to a person, and a list of open capture streams cannot. Turning one into pixels is the platform's job, and happens only when a position actually takes it.
func Without ¶
Without returns the offers with the ones showing these displays left out.
It is how the desk keeps its OWN screen out of its own band. The glasses are a display like any other to the window server, so the sources list offers them -- and a position showing them shows this program's window, which is showing that position: a picture inside itself. Measured, with a session whose virtual displays were refused and which fell back to the two screens the machine had: "screen 2: display 3", display 3 being the glasses.
A display that is not offered is not merely hidden from the cycle: it cannot be assigned, so nothing can put it back by accident.
type Options ¶
type Options struct {
// Screens is how many virtual screens to put on the ribbon. Zero asks for
// as many as fit round the circle without overlapping.
Screens int
// Distance is how far the band sits from the viewer, as a multiple of the
// distance at which one screen fills the view. Zero, like one, is the near
// end. See [Plan.Distance].
Distance float64
// SplayDeg is the angle between one screen and the next, in degrees. A
// NEGATIVE value asks for the flat band; zero asks for [DefaultSplayDeg],
// because zero is what a caller that has not thought about it passes.
SplayDeg float64
// FOVDeg overrides the catalogue's horizontal field of view, in degrees.
// It exists because the catalogue is honest about what it does not know, and
// because a person who measures their own optics should be able to say so.
FOVDeg float64
// USB is what the headset says about itself over the bus, when the caller
// has looked. It may be nil.
//
// It is STRONGER evidence than the display name and is consulted first. A
// display name is whatever the panel puts in its EDID, and a dock, a
// capture card or a KVM in the path can replace it with something generic;
// a USB product id names a model, and for some brands it is the only thing
// that does. On these very glasses the bus says "XREAL 1S" while the
// display is not up at all.
USB *glasses.USB
}
Options are the choices a person makes.
type Picture ¶
Picture is one frame to be written, in the shape both a camera and a canvas already have.
BGRA, because that is what everything upstream of it carries: AVFoundation delivers BGRA and the desk's own canvas is painted through a BGRA painter. Converting on the way in would be a second copy of every frame to save one byte-swap on the few that are ever written.
type Placement ¶
Placement is one line of a desk: an application, and the position on the ribbon its windows belong on.
func Spread ¶
Spread hands out one screen per application, in order, up to screens.
One press, and a desk that was showing six empty desktops is showing six applications. What it will NOT do is wrap: an application past the last screen is left exactly where it is, because moving two applications onto one screen hides one of them, and a person who pressed one key cannot be expected to guess which.
Applications already on a screen are placed too, and deliberately: the point of the key is that the desk ends up in a state a person can predict — the first application on screen 1, the second on screen 2 — not that it ends up in whatever state the previous arrangement plus this press produces.
The result is [Placement]s, so the live path is the one the settings file already uses. It never returns more placements than there are screens.
type Plan ¶
type Plan struct {
// Model is the headset this was worked out for.
Model string
// How says what named the model, so a caller can show whether the answer
// came from the bus, from the display, or from the person at the keyboard.
How glasses.How
// ScreenW and ScreenH are the pixel size to create each virtual display at:
// one eye's viewport, which is the most the glasses can show at once.
ScreenW, ScreenH int
// Stereoscopic reports whether the display is in a side-by-side 3D mode, in
// which case the two eyes get different pixels of the same frame.
Stereoscopic bool
// HFOVDeg and VFOVDeg are one eye's field of view, in degrees.
HFOVDeg, VFOVDeg float64
// Layout places the screens. DensityDeg is derived so that a screen of the
// eye's own shape spans exactly HFOVDeg.
Layout ribbon.Layout
// contains filtered or unexported fields
}
Plan is everything the renderer needs, worked out from the headset itself.
The rule that shapes all of it is the one the viewer asked for: ONE SCREEN IS ONE FULL VIEW. A virtual screen is created at exactly one eye's resolution, and given exactly the arc that eye can see, so that looking straight at a screen shows it edge to edge, at one source pixel per output pixel.
func (Plan) Distance ¶
Distance is how far the band sits from the viewer, as a multiple of the distance at which one screen fills the view exactly.
One is the near end and the doctrine this started from: a screen is the whole view, at one source pixel per panel pixel, and its neighbours are off to the sides where the head has to turn to reach them. Two puts each screen in half the width, so the two beside it are visible without turning. That is the whole of what "further away" means here -- a screen does not move, it takes up less room, which is what moving a monitor back does.
It is deliberately not allowed below one. Closer than filling the view means seeing PART of a screen, and a desk whose middle screen is cropped is not a desk anyone asked for.
func (Plan) ScreenWidth ¶
ScreenWidth is how wide screen i is, which is Plan.ScreenW unless it has been given a shape of its own.
func (Plan) Screens ¶
Screens is the ribbon's screens: each one a whole view of the glasses, except any that has been given a shape of its own by Plan.WithScreenWidth.
func (Plan) SplayDeg ¶
SplayDeg is the angle between one screen and the next, in degrees.
Zero is the flat band: every screen in one plane, square on, which is what this package drew before there was an angle at all and still the right answer for somebody who wants a single wide surface. Anything more turns each screen towards the viewer, the way the two beside the middle one on a desk of three monitors are turned.
It is an angle between NEIGHBOURS and not a total arc, so it means the same thing whatever the screen count -- which is the point: three screens at twenty degrees and nine at twenty degrees have the same feel in front of you and differ in how far round the rest of them go.
The field cannot be negative -- Plan.WithSplay clamps at nothing and it is the only way in -- so there is no guard here to read past. A plan's zero value is the flat band, which is the right thing for it to be.
func (Plan) String ¶
String renders the plan the way a person would want it logged.
A field of view of zero is not printed as "0.00°": it means NOT KNOWN, and a number is a claim.
func (Plan) WithDistance ¶
WithDistance is this plan seen from a different distance, clamped to 1..MaxDistance.
Nothing about the screens changes: the band is the same ring of the same panels at the same resolution. What changes is the number of pixels each one occupies in the view, which is the pixel scale of the whole band -- so this is one multiplication in one place, and the navigator, the gallery and the captures know nothing about it.
func (Plan) WithScreenWidth ¶
WithScreenWidth returns the plan with screen i that many pixels wide, at the same height as every other screen. A width of zero or less puts it back to the shape of the glasses.
The plan is copied, widths and all: a plan handed out and then changed under its holder is a band and a navigator disagreeing about where a screen is.
func (Plan) WithScreens ¶
WithScreens is this plan with a different number of screens on the band, clamped to 1..MaxScreens.
The screens are FLAT, so the angles are a scroll coordinate and nothing more, which is what lets one be added while the desk is running.
A curved band had to fit in 360 degrees, and at one screen per view that was seven of them and no more. Flat, the circle is a fiction: the yaw says how far along the band the viewer is, and the band is however long it needs to be. So n screens are spread over the full turn whatever n is -- and the ceiling is therefore a DECISION, not a consequence. See MaxScreens.
func (Plan) WithSplay ¶
WithSplay is this plan with a different angle between neighbours, clamped to 0..MaxSplayDeg.
Like the distance, it changes nothing about the screens themselves -- same count, same resolution, same order. It changes which way each one faces.
type RunOptions ¶
type RunOptions struct {
// Title names the window. Nobody sees it in the glasses; a window manager
// and an accessibility tree do.
Title string
// Screen is the display to take over.
Screen glasses.Display
// For stops the session after this long. Zero runs until the viewer quits,
// which is what a person wants and what a test does not.
For time.Duration
// Logf receives progress. A nil Logf says nothing.
Logf func(string, ...any)
// Shortcuts are the system-wide combinations to claim. Nil asks for
// [DefaultShortcuts].
Shortcuts []Shortcut
// Hotkeys is how to claim them — chiefly the fallback ladder. Nil asks for
// [DefaultLadder].
Hotkeys *hotkey.Options
// OnGranted receives the combination each action ended up with, once they
// have been claimed.
//
// It exists because a claim is not a grant. The menu-bar item is made before
// any session and outlives every one of them, so it cannot know what the
// window server will hand over -- and the ladder substitutes when a
// combination is taken, which is exactly the case a person cannot guess. So
// the answer is sent out when it is known rather than predicted beforehand.
//
// Nil is nobody asking, which is the default.
OnGranted func(map[Action]hotkey.Combo)
// Actions are actions from somewhere other than the keyboard: a menu-bar
// item, a script, a remote. They are treated exactly like a global shortcut
// -- the same actions, the same loop -- because the difference between
// pressing a key and choosing a menu row is the caller's business and not
// this loop's.
//
// Nil is no such source, which is the default.
Actions <-chan Action
// DepthModel names a Core ML depth model — an .mlpackage or a compiled
// .mlmodelc — for the 3D conversion. Empty falls back to depth guessed
// from the picture itself, which needs nothing and is visibly worse.
//
// It is opened only when a viewer asks for 3D, and closed when they stop:
// the model costs a GPU context and a second of loading, and a desk that
// never turns it on should pay neither.
DepthModel string
// Stereo3D starts with the conversion already on.
Stereo3D bool
// Badge is how long the screen's number stays up after the band moves, in
// seconds. Zero turns it off.
Badge float64
// Windowed leaves the glasses display's own menu bar and Dock on top of the
// picture instead of covering them. See [Config.Immersive].
Windowed bool
// Interactive lets the desk's own window take the keyboard and the mouse.
//
// It does NOT by default, and that is the whole design rather than a
// precaution. The desk is a picture of screens that applications are running
// on: the keyboard has to reach THOSE applications, and a window that takes it
// is a window that stops the person using their own desk. The pointer is
// worse -- it wanders onto the display the desk owns, where the picture is a
// capture of somewhere else and so does not show where the mouse is. Measured:
// the way out was unplugging the glasses.
//
// So the desk is driven from outside, which is what the system-wide shortcuts
// and the menu-bar item are for, and the applications keep the keyboard and
// the pointer they always had.
//
// Interactive is for a session on a desktop -- with -windowed, to try the
// thing out -- where clicking the picture is the only way in.
Interactive bool
// NoGlobal leaves the system-wide shortcuts unclaimed.
//
// Claiming them takes them away from everything else on the machine for as
// long as the session lasts, which is what makes them useful and what makes
// them worth being able to refuse.
NoGlobal bool
// Screens are the desk's displays in ribbon order, so the band can follow
// the pointer onto one of them. Nil turns that off: without the ids there is
// no way to tell one of the desk's screens from the machine's own.
//
// It is the arrangement at START-UP. See [RunOptions.Showing] for what a
// position is showing NOW, which is not the same thing the moment somebody
// mirrors this Mac's own panel onto the ribbon.
Screens []uint64
// Showing answers, per ribbon position, the display it is showing right now,
// or 0 for a position showing nothing.
//
// Without it the band follows the pointer onto the displays this program
// MADE and nothing else -- so a position mirroring this Mac's own screen is
// a position the band will not follow the pointer onto, and moving the mouse
// there loses it in exactly the way following was written to prevent. The
// list changes while it runs, so it is asked for rather than handed over.
//
// Nil falls back to [RunOptions.Screens].
Showing func() []uint64
// Snapshot, when set, is handed the first frame actually drawn — the picture
// the glasses were shown. It is written by the caller, so this package never
// decides where a capture of somebody's screens lands.
Snapshot func(pix []byte, w, h int)
}
RunOptions are the choices a caller makes about the session itself, as opposed to the arrangement, which is the Plan's business.
type Screens ¶
type Screens struct {
// IDs identify the displays, in ribbon order.
IDs []uint64
// Virtual reports whether these were created by us. Never true here.
Virtual bool
// Why explains what happened, and is worth showing to a person.
Why string
}
Screens is what the ribbon will show, and how it was obtained.
On Linux it is always the displays the machine already has. Creating one needs a compositor to agree, and no protocol an ordinary program can speak offers it — so unlike macOS, where a private CoreGraphics call makes real ones, here the ribbon carries what is already there. Fewer screens, everything else the same.
func (*Screens) Add ¶
Add creates one more display and appends it.
Nothing here can: creating a display is the one part of this that has no portable answer, and only macOS is wired up. A person is told rather than left pressing a key that does nothing.
type SettingsOptions ¶
type SettingsOptions struct {
// Attached are the headsets to choose between. Nil asks the bus.
Attached []glasses.USB
// DisplayH is the height in pixels of the display the window will appear
// on, so its type can be magnified to the same SIZE TO LOOK AT there. Zero
// leaves the scale alone.
//
// A 5x7 bitmap glyph on a 2160-row panel is three millimetres tall, which
// is how this window came to be reported as too small on an 8K screen.
DisplayH int
// Logf receives progress. A nil Logf says nothing.
Logf func(string, ...any)
}
SettingsOptions are the choices a caller makes about the settings window.
type Shortcut ¶
Shortcut is one system-wide combination and what pressing it does.
System-wide is the whole point. A virtual desktop in glasses is used while something else has the keyboard: the viewer is reading in one screen and wants the next one, without first clicking on a window they cannot see. A shortcut that only works when xrdesk is frontmost would be a shortcut for nothing.
func DefaultShortcuts ¶
func DefaultShortcuts() []Shortcut
DefaultShortcuts are what the ribbon needs from the whole machine.
ONE prefix for all of them: Control+Option+Command. It was not so at first — the band was on ⌥⌘← and ⌥⌘→ while everything added later took the third modifier — and a person who had learnt the desk pressed ⌃⌥⌘← for the band and got nothing at all. A set of shortcuts with two prefixes is a set nobody can remember, and the one that gives way is the one with fewer keys on it.
⌥⌘← and ⌥⌘→ were also Safari's tab navigation, which the desk was quietly taking for the length of a session.
There is no TOGGLE here any more, and that is the point of the set: every one of these is pressed BLIND, so each says what it does. ⌃⌥⌘↑ shows the screens, ⌃⌥⌘↓ shows what is running on them, and leaving is the bare Escape the gallery holds while it is up. ⌥⌘Space used to toggle the screen gallery, which meant "open" or "close" depending on something the person could not see -- and it was the Finder's search window on a stock macOS, so it never got the keys it asked for either. The toggle remains on `g` INSIDE the window, where the state is in front of you, and a settings file can still put it back.
See DefaultLadder, and Hotkeys.Describe for what was actually granted.
func GalleryShortcuts ¶
func GalleryShortcuts() []Shortcut
GalleryShortcuts are the BARE keys a gallery claims for as long as it is up.
A person looking at a grid of screens or of applications should not have to hold three modifiers to walk it -- "in the gallery, moving with just the arrows ought to be enough", which is exactly right. So while a gallery covers the view, the arrows, Return and Escape mean what they look like they mean.
They are claimed system-wide, because the desk's window deliberately does not take the keyboard. That is a serious thing to do to a machine — a bare arrow claimed for ever would break typing everywhere — so it is done only while a gallery is up and undone the moment it closes. go-macos/hotkey v0.6.0 asks a caller to say so with Options.BareKey rather than allowing it by accident.
Nobody is typing into anything while a gallery covers their view.
type Slant ¶
type Slant struct {
// Screen is the index of the screen on the band. Like a blit's, it is not
// unique within a frame: a screen straddling the seam comes back twice.
Screen int
// Dst is the bounding box in the canvas. Every column of it is written
// between its own two rows; the rest of the box is left alone.
Dst stereo.Rect
// Cols is one entry per column of Dst, in order.
Cols []SlantCol
}
A Slant is one screen drawn TURNED TOWARDS THE VIEWER: a flat panel rotated about the vertical axis, projected.
It is what a desk of monitors looks like. The one in front is square on; the ones beside it are angled in, so their far edge is shorter than their near edge and their surface is foreshortened. That is not the curvature this package deleted -- a curve bows the screen you are looking AT, which argues with the depth the glasses already present. A rotation leaves every screen flat and only changes which way it faces, which is the difference between a bent screen and a turned one.
The shape is a TRAPEZOID and that is not an approximation: a plane rotated about the vertical projects to one exactly. Vertical edges stay vertical -- the rotation does not move a point up or down -- so a source column lands on a destination column, and the only things that vary along the width are how tall that column is and which source column it reads. Nothing here needs a warp table, a matrix per pixel, or a panorama.
At zero rotation the two vertical edges are the same depth, so the height is constant and the source columns are evenly spaced: the trapezoid IS the rectangle this package drew before, byte for byte. That is asserted rather than asserted-in-a-comment; see TestASlantAtZeroDegreesIsTheOldRectangle.
type SlantCol ¶
type SlantCol struct {
// Src is the source column, or -1 for a column with no source at all --
// which happens where a rotated panel has turned past the edge of the
// source. Such a column is left as background rather than stretched.
Src int32
// Y0 and Y1 are the destination rows this column covers, [Y0, Y1).
Y0, Y1 int32
}
SlantCol is one destination column of a slant: which source column it shows, and the destination rows it covers.
Rows rather than a scale, because the scale is what a reader would have to work out from them and the rows are what the drawing loop wants. Y0 and Y1 are absolute canvas rows, so a column can be clipped at the top or the bottom of the canvas without the loop knowing what clipping is.
type Source ¶
Source is one captured screen's pixels.
Stride is carried rather than assumed because it is NOT width*4 in general: a 400-pixel-wide capture came back from ScreenCaptureKit with rows padded to 1664 bytes, while a 3840-pixel-wide one had no padding at all. Code that assumes either is wrong half the time, and wrong here means a picture that shears progressively down the screen.
type Strip ¶
type Strip struct {
// contains filtered or unexported fields
}
A Strip is the ribbon with its screens left FLAT.
The curved ribbon puts each screen on the surface of a cylinder, which is geometrically honest and, worn, buys nothing: a screen the viewer is looking straight at is drawn with a bow in it, and the bow argues with the depth the glasses are already presenting. It also costs a projection — an equirectangular panorama and a per-pixel warp, 2.8 ms of a 16.6 ms frame — to produce a picture whose whole purpose is to look like a flat screen.
So the screens are laid side by side on a flat band and the band slides.
The scale is the only thing that has to be decided, and the plan decides it: ONE VIEW IS ONE FIELD OF VIEW. Everything else follows from the ribbon's own placement — where each screen sits, how wide it is, and therefore how much space is left between two of them. There is no separate gap to set and get wrong, because the gap is not this type's to invent.
The band closes on itself: walking right past the last screen arrives at the first, and a screen straddling that join is drawn as two pieces, one against each edge.
func NewStrip ¶
NewStrip lays the ribbon's screens out flat for a view of viewW x viewH.
totalPx is how long the whole band is, in pixels. That single number sets the scale: a screen's arc becomes its width, and the arc between two of them becomes the space between them. Nothing here needs a field of view — the band is flat, so how large a screen LOOKS is the optics' business and not this package's.
func (*Strip) Frame ¶
Frame appends the blits for one view of the band and returns the extended slice. Passing dst[:0] of the previous frame's slice reuses the storage, and the frame then allocates nothing at all.
The destination rectangles are in VIEW coordinates and already clipped, so the canvas a caller composes into is the size of the picture rather than of a panorama it would then have to be a projection of.
func (*Strip) Fullscreen ¶
Fullscreen appends the blit for one screen filling the whole view.
It is a real promotion only when the screen is narrower than the view. At one screen per view it changes nothing, which is the right answer rather than a missing feature: there is nowhere for a screen already filling the glasses to grow to.
func (*Strip) Offset ¶
Offset turns a yaw in radians into the point on the band the viewer faces.
The navigator thinks in angles because a ribbon is a circle, and it keeps working — the gallery, the focus, the shortest way round — without knowing that the picture is flat. A full turn is the whole band.
func (*Strip) SetSourceWidths ¶
SetSourceWidths gives screens their own source widths.
A screen mirroring a display this program did not make is not the shape of the glasses -- this Mac's panel is 1.547 against the band's 1.778 -- so its capture is that shape too, and the arc it takes on the ribbon was worked out from the same number. Mapping it through the band's width instead would stretch it across a quad that is not its shape, which is the very thing giving that screen its own arc was for.
A nil or short slice, or an entry of zero, leaves that screen on the width every other screen has.
func (*Strip) Toward ¶
Toward is how far the band has moved from the focused screen towards its neighbour, in screens: 0 is the focused screen centred, 0.5 half way to the next one, -0.25 a quarter of the way back to the last.
It exists so that the turned band (Fan) and the flat one agree about where the band IS, and it is expressed RELATIVE TO THE FOCUS on purpose. An absolute position along the band would need the screens to be in band order, and they are not: a ribbon may put screen zero anywhere on the circle, and this desk's starts at 210 degrees. Asking "how far past the screen the navigator says we are on" needs no such assumption -- and taking each screen's place from the ribbon rather than assuming an even spread is what keeps this in step with the navigator, which it once was not.
type Tray ¶
type Tray struct {
// contains filtered or unexported fields
}
Tray is the desk's menu-bar item, and the two ways it can live.
func OpenTray ¶
OpenTray puts the item in the menu bar and sends what is chosen to actions.
THE ITEM IS go-widgets/tray's, NOT THIS PACKAGE'S. It was mine for a while, built straight on NSStatusItem, and that was a mistake with a symptom: a status item needs an NSApplication whose run loop is RUNNING, and this program has none until a display has been chosen and a window opened on it. Lending AppKit slices of the main thread drew the icon and never opened its menu -- "l'icon est visible dans le tray mais je n'ai aucun menu" -- because a menu is not drawn, it is tracked, and tracking needs the loop.
tray owns that distinction: Run when a program has no loop of its own and Attach when it does. Both are its backends' business on three platforms rather than this package's on one.
The send is NON-BLOCKING, and that is the whole of the design. A menu handler runs while the desk may be stopped -- the settings window is up, or the ribbon is between sessions -- so nobody is reading. A blocking send would leave that handler parked for as long as the window is open and then replay every click at once. A dropped choice is logged and forgotten, which is what a person clicking a menu with nothing happening expects: the next click, not the last five.
func (*Tray) Attach ¶
Attach adds the item to a loop somebody else is already running, and returns at once. It is for the desk: the window owns the main thread from then on.
func (*Tray) Hold ¶
Hold runs the item AND the platform's main loop, and returns when Release is called.
It is for the wait: there is no window yet, so nothing else is driving AppKit, and without a loop the item is an object nobody can open. It must be called on the main thread.
func (*Tray) Release ¶
func (t *Tray) Release()
Release stops the loop Hold is running, so the caller can go on to open a window and drive its own.
func (*Tray) ShowShortcuts ¶
ShowShortcuts puts the combination that was GRANTED on each row it belongs to, and rebuilds the menu.
Granted rather than asked for, which is why this is a method called later instead of an argument to OpenTray. The item is made once for the whole process and outlives every session, while the shortcuts are claimed when a session starts -- and a claim is not a grant: the ladder substitutes when a combination is taken, so a menu built from what was ASKED for would print a combination that does nothing. A row whose action was never granted keeps its bare label rather than a lie.
The rows a person can reach with the keyboard say so here rather than in the settings window, which is where they used to be listed: a menu row and the key that does the same thing belong on the same line, and moving them takes most of a page out of a window that had grown too tall for a laptop screen.
func (*Tray) State ¶
func (t *Tray) State() *mvvm.Observable[TrayState]
State is what the icon follows.
An Observable rather than a setter, because that is how anything in this fleet says "this changed" across a boundary, and because tray.BindIcon takes one: the icon then follows the desk without either knowing about the other.
type TrayRow ¶
type TrayRow struct {
Title string
Key string
Action Action
// Symbol is the system symbol drawn to the left of the label, by the name
// the platform knows it under -- an SF Symbol on macOS. Empty leaves the
// row text-only, which is what a separator is and what every row was.
//
// A NAME RATHER THAN A PICTURE, for the same reason the menu-bar item
// carries one: a symbol is the platform's own, so it arrives at the weight
// and in the ink of every other menu in the bar, follows a light or dark
// appearance without being asked, and costs no bytes in the binary. An icon
// pack would be four hundred pixels of somebody else's drawing beside
// nineteen of Apple's -- which is exactly the report the menu-bar icon was
// already fixed for once.
//
// A platform with no such symbol draws the row as it always did.
Symbol string
}
TrayRow is one row of the menu: what it says, its key equivalent, and the action it asks for. An ActionNone row is a separator.
func TrayRows ¶
func TrayRows() []TrayRow
TrayRows is the menu, in order.
Deliberately short. It is not a second copy of the keyboard: turning the band and moving in the gallery are done while looking at the band, by feel, and a row in a menu bar for either would be a row nobody uses. What is here is what a person cannot do from inside the glasses.
Source Files
¶
- apps.go
- appsview.go
- await.go
- badge.go
- camera.go
- canvas.go
- config.go
- crash.go
- darkstate.go
- desk.go
- dim.go
- dim_other.go
- dim_read_other.go
- fan.go
- fence.go
- fence_other.go
- gallerymark.go
- gestures.go
- grid.go
- hotkeys.go
- inventory.go
- notice.go
- own_display.go
- photo.go
- plan.go
- platform_linux.go
- point_other.go
- remember.go
- render.go
- run_display.go
- save.go
- send.go
- send_other.go
- settings.go
- settings_display.go
- slant.go
- strip.go
- tray.go
- traydesk.go
- trayicon_other.go
- traykey.go
- usb.go
- usb_linux.go
- usb_sysfs.go
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
deskchaos
command
Command deskchaos runs the desk over and over, breaks it on purpose, and checks what it leaves behind.
|
Command deskchaos runs the desk over and over, breaks it on purpose, and checks what it leaves behind. |
|
deskcheck
command
deskcheck says what this machine can actually do, and proves each answer by doing it rather than by reporting a capability bit.
|
deskcheck says what this machine can actually do, and proves each answer by doing it rather than by reporting a capability bit. |
|
macapp
command
Command macapp assembles xrdesk into a macOS application bundle.
|
Command macapp assembles xrdesk into a macOS application bundle. |
|
standin
command
Command standin holds a virtual display open under a headset's name, so the desk can be exercised with no glasses on the desk.
|
Command standin holds a virtual display open under a headset's name, so the desk can be exercised with no glasses on the desk. |
|
xrdesk
command
xrdesk shows several screens on a 360° ribbon inside AR glasses.
|
xrdesk shows several screens on a 360° ribbon inside AR glasses. |
