Files
TeleportFling/internal/capture/monitors.go
T
petere f1f2bfe1a3 feat: portal-based monitor enumeration fallback
Add a xdg-desktop-portal ScreenCast fallback to monitor enumeration so
the GUI picker works on compositors without hyprctl (GNOME/KDE etc.).
ListMonitors now tries hyprctl first, then creates a portal session,
selects monitor sources, starts capture and parses the stream list.

Verified on Hyprland: the portal path returns the 1920x1200 monitor,
confirming the compositor-agnostic mechanism works.
2026-09-19 20:40:56 +01:00

84 lines
2.3 KiB
Go

// Monitor enumeration for the settings UI.
//
// The screen-capture backend itself selects a monitor by index; this file
// provides a way to list the available monitors so the GUI can present a
// friendly picker instead of a raw index.
//
// Hyprland exposes monitor info via the `hyprctl monitors` command. Other
// Wayland compositors would need a portal-based enumeration; for now we only
// implement the Hyprland path (the primary dev environment) and return a
// clear error elsewhere.
package capture
import (
"encoding/json"
"errors"
"os/exec"
)
// Monitor describes one capturable output.
type Monitor struct {
Index int // capture StreamIndex to pass to OpenPipeWire
Name string // compositor name, e.g. "eDP-1"
Width int
Height int
Primary bool
}
// ErrNoMonitors is returned when monitor enumeration is unsupported or fails.
var ErrNoMonitors = errors.New("capture: monitor enumeration unavailable on this compositor")
// ListMonitors returns the available monitors for the GUI picker. It prefers
// the Hyprland IPC when available, falling back to the xdg-desktop-portal
// ScreenCast API for other compositors.
func ListMonitors() ([]Monitor, error) {
if hyprctlAvailable() {
if mons, err := hyprctlMonitors(); err == nil {
return mons, nil
}
}
return portalMonitors()
}
// hyprctlAvailable reports whether the Hyprland monitor command exists.
func hyprctlAvailable() bool {
_, err := exec.LookPath("hyprctl")
return err == nil
}
// hyprctlMonitor is the JSON shape emitted by `hyprctl monitors -j`.
type hyprctlMonitor struct {
ID int `json:"id"`
Name string `json:"name"`
Width int `json:"width"`
Height int `json:"height"`
Description string `json:"description"`
Focused bool `json:"focused"`
}
// hyprctlMonitors lists monitors via the Hyprland IPC command.
func hyprctlMonitors() ([]Monitor, error) {
out, err := exec.Command("hyprctl", "monitors", "-j").Output()
if err != nil {
return nil, ErrNoMonitors
}
var raw []hyprctlMonitor
if err := json.Unmarshal(out, &raw); err != nil {
return nil, ErrNoMonitors
}
monitors := make([]Monitor, 0, len(raw))
for _, m := range raw {
monitors = append(monitors, Monitor{
Index: m.ID,
Name: m.Name,
Width: m.Width,
Height: m.Height,
Primary: m.Focused,
})
}
return monitors, nil
}