Documentation
¶
Overview ¶
Package portal implements the shared plumbing for talking to the XDG desktop portal (org.freedesktop.portal.Desktop) that internal/uiauto's Linux Wayland input (internal/uiauto/input) and screen capture (internal/uiauto/screen) packages both need: the well-known bus/object names, org.freedesktop.portal.Request handle-token generation and Response-signal handling, response-code -> core.DesktopError mapping, and consent timeouts.
Before this package existed, input_linux.go and screen_linux.go each duplicated this plumbing, which is exactly what made it impossible for them to share one RemoteDesktop+ScreenCast session (see the Wayland parity plan, block W1/W2): a portalInput.MoveMouse call addressed ScreenCast stream id 0, which is never a valid stream absent a real ScreenCast session negotiated on the SAME session handle as the pointer device selection. This package makes that one-session flow possible; see Session/Open in session.go.
No file in this package uses cgo.
Index ¶
- Constants
- func LoadRestoreTokens() (remoteDesktop, screenCast string)
- func NewHandleToken(prefix string) string
- func Request(ctx context.Context, caller RequestCaller, path dbus.ObjectPath, ...) (map[string]dbus.Variant, error)
- func ResponseCodeError(step string, code uint32) error
- func SaveRestoreTokens(remoteDesktop, screenCast string) error
- func SetCurrent(sess *Session)
- type OpenOptions
- type RequestCaller
- type Session
- type Stream
Constants ¶
const ( BusName = "org.freedesktop.portal.Desktop" ObjectPath = dbus.ObjectPath("/org/freedesktop/portal/desktop") RequestIface = "org.freedesktop.portal.Request" SessionIface = "org.freedesktop.portal.Session" RemoteDesktopIface = "org.freedesktop.portal.RemoteDesktop" ScreenCastIface = "org.freedesktop.portal.ScreenCast" ScreenshotIface = "org.freedesktop.portal.Screenshot" )
Well-known XDG desktop portal bus/object/interface names.
const ( DeviceKeyboard uint32 = 1 DevicePointer uint32 = 2 )
RemoteDesktop.SelectDevices DeviceType bit flags.
const ( PersistNone uint32 = 0 PersistUntilLogout uint32 = 1 PersistUntilRevoked uint32 = 2 )
SelectDevices/SelectSources persist_mode values. PersistUntilRevoked is what W3 (consent persistence) needs: without it the compositor forgets the grant as soon as the session ends and the user is re-prompted on every single Pando run.
const ConsentTimeout = 30 * time.Second
ConsentTimeout bounds how long a caller waits for the user to respond to a portal consent dialog (or for the portal to fail outright when no compositor/portal backend is running at all).
const (
SourceMonitor uint32 = 1
)
ScreenCast.SelectSources SourceType bit flags.
Variables ¶
This section is empty.
Functions ¶
func LoadRestoreTokens ¶
func LoadRestoreTokens() (remoteDesktop, screenCast string)
LoadRestoreTokens returns the persisted RemoteDesktop/ScreenCast restore tokens, or two empty strings if none are stored yet (fresh machine, or the store is unavailable/unreadable — read failures are treated the same as "no token", never a hard error: the caller degrades to a fresh consent prompt).
func NewHandleToken ¶
NewHandleToken exposes newHandleToken to other packages in this module (internal/uiauto/screen's Screenshot portal call also needs a unique handle_token per the XDG portal spec, outside of a Session).
func Request ¶
func Request(ctx context.Context, caller RequestCaller, path dbus.ObjectPath, iface, method string, args ...interface{}) (map[string]dbus.Variant, error)
Request performs one Request-pattern portal call and maps its outcome: a transport-level error (portal missing, D-Bus unreachable) is reported as PLATFORM_NOT_SUPPORTED, a non-zero response code is mapped via ResponseCodeError, and success returns the raw results.
func ResponseCodeError ¶
ResponseCodeError maps an org.freedesktop.portal.Request response code to a *core.DesktopError. Per the XDG portal spec: 0 is success (nil is returned); 1 means the user explicitly cancelled/declined the request, mapped to PERM_DENIED so the LLM gets an actionable "the user said no" signal rather than a generic failure; any other non-zero code (2 = "ended"/other error, or a backend-specific code) is reported as ACTION_FAILED, since the portal itself was reachable but the operation could not complete.
func SaveRestoreTokens ¶
SaveRestoreTokens atomically persists the given restore tokens, so the next Session.Open on this machine can pass them back and skip the consent dialog. Passing an empty string for either leaves that field cleared (a token was rejected/expired, or the compositor did not grant one).
func SetCurrent ¶
func SetCurrent(sess *Session)
SetCurrent registers sess as the process-wide shared portal session. Passing nil clears it. Safe for concurrent use.
Types ¶
type OpenOptions ¶
type OpenOptions struct {
// WantScreenCast, when true, also negotiates ScreenCast.SelectSources
// on the same session handle, which is what gives
// NotifyPointerMotionAbsolute a coordinate space (Session.Streams).
// When false, only RemoteDesktop devices are selected: relative
// pointer motion and keyboard events work, but absolute positioning
// does not (there is no stream to address).
WantScreenCast bool
// RemoteDesktopRestoreToken/ScreenCastRestoreToken, when non-empty,
// are passed back to the portal so a previously-granted consent can be
// restored without a fresh prompt (W3). A rejected/expired token
// degrades to a fresh consent prompt automatically: see Open.
RemoteDesktopRestoreToken string
ScreenCastRestoreToken string
// ConsentTimeout overrides ConsentTimeout when non-zero.
ConsentTimeout time.Duration
}
OpenOptions configures Open.
type RequestCaller ¶
type RequestCaller interface {
// Request invokes path's iface.method(args...), which per the XDG
// portal convention must return an org.freedesktop.portal.Request
// object path, and waits (bounded by ctx) for that Request's Response
// signal, returning its response code and results.
Request(ctx context.Context, path dbus.ObjectPath, iface, method string, args ...interface{}) (code uint32, results map[string]dbus.Variant, err error)
// Call invokes a plain (non-Request) D-Bus method on path and returns
// its raw reply body.
Call(ctx context.Context, path dbus.ObjectPath, iface, method string, args ...interface{}) ([]interface{}, error)
// Close releases the underlying D-Bus connection.
Close() error
}
RequestCaller abstracts the D-Bus surface this package's session/request logic depends on, so tests can substitute a fake in-memory portal instead of a real xdg-desktop-portal backend (none is running on the dev box this was built on: no DISPLAY/WAYLAND_DISPLAY, no portal).
func Dial ¶
func Dial() (RequestCaller, error)
Dial connects to the D-Bus session bus for the desktop portal. Callers own the returned RequestCaller's lifetime and must Close it.
type Session ¶
type Session struct {
Streams []Stream
// RemoteDesktopRestoreToken/ScreenCastRestoreToken are the restore
// tokens the portal returned from this Start call, when persist_mode
// was honoured. Empty when the compositor does not support restore
// tokens or none was granted.
RemoteDesktopRestoreToken string
ScreenCastRestoreToken string
// contains filtered or unexported fields
}
Session is one combined RemoteDesktop+ScreenCast portal session: a single session_handle produced by RemoteDesktop.CreateSession, against which SelectDevices (keyboard/pointer) and, when requested, ScreenCast.SelectSources were both issued before Start. This is W1/W2 of the Wayland parity plan: only a session negotiated this way has a coordinate space (Streams) that NotifyPointerMotionAbsolute can meaningfully address.
func Current ¶
func Current() *Session
Current returns the process-wide shared portal session, or nil if none has been established yet.
func Open ¶
func Open(ctx context.Context, caller RequestCaller, opts OpenOptions) (*Session, error)
Open establishes ONE combined RemoteDesktop(+ScreenCast) portal session: CreateSession, then SelectDevices and (when WantScreenCast) SelectSources against that SAME session handle, then Start. This is the fix for the pre-existing bug where MoveMouse addressed a hardcoded, nonexistent stream id 0: only a session negotiated this way has a real coordinate space to address.
When a restore token was supplied and the attempt using it fails, Open retries once with a fresh (tokenless) session rather than returning a hard failure — a rejected or expired restore token must degrade to a new consent prompt, not break the feature outright.
func (*Session) Close ¶
func (s *Session) Close()
Close releases the session's D-Bus objects: it asks the portal to close the session (best effort — the portal may already have torn it down) and then closes the underlying connection. It does not clear the process-wide Current() session; callers that set it should also clear it (SetCurrent(nil)) when appropriate.
func (*Session) Handle ¶
func (s *Session) Handle() dbus.ObjectPath
Handle returns the session_handle object path, the first argument every RemoteDesktop/ScreenCast notify/select call needs.
type Stream ¶
Stream is one ScreenCast stream from a Start response: a PipeWire node id plus the position/size rectangle it covers in the compositor's global (absolute pointer) coordinate space. This rectangle is exactly the coordinate space org.freedesktop.portal.RemoteDesktop. NotifyPointerMotionAbsolute needs: the portal has no meaning for "absolute (x,y)" without a stream saying which capture region those coordinates are relative to.