feat: add monitor picker to the GUI settings

Enumerate monitors via hyprctl (Hyprland) and present them as a
dropdown in the settings window instead of a raw index. The picker is
enabled only for the screen source and persists the selected monitor as
stream_index. Falls back to a single indexed option when enumeration is
unavailable (non-Hyprland).
This commit is contained in:
2026-09-19 10:13:23 +01:00
parent 50955fd606
commit 5663fb6b36
3 changed files with 191 additions and 2 deletions
+70 -2
View File
@@ -8,6 +8,7 @@ package gui
import (
"errors"
"fmt"
"image/color"
"log"
"strconv"
@@ -22,6 +23,7 @@ import (
"fyne.io/systray"
"teleportfling/assets"
"teleportfling/internal/capture"
"teleportfling/internal/config"
"teleportfling/internal/flinger"
)
@@ -63,6 +65,8 @@ type App struct {
audioChk *widget.Check
announceChk *widget.Check
srcSel *widget.Select
monSel *widget.Select
monitors []capture.Monitor
statsDone chan struct{}
}
@@ -133,6 +137,7 @@ func (g *App) buildUI() {
// Source.
g.srcSel = widget.NewSelect([]string{"screen", "pattern"}, func(string) {})
g.srcSel.SetSelected(g.cfg.Source)
g.setupMonitorPicker()
// Quality.
g.qualitySel = widget.NewSelect([]string{"50", "60", "70", "80", "90", "100"}, func(string) {})
@@ -167,6 +172,7 @@ func (g *App) buildUI() {
{Text: "Name", Widget: g.nameEnt},
{Text: "Port", Widget: g.portEnt},
{Text: "Source", Widget: g.srcSel},
{Text: "Monitor", Widget: g.monSel},
{Text: "Quality", Widget: g.qualitySel},
{Text: "Frame rate", Widget: g.fpsSel},
{Text: "", Widget: g.audioChk},
@@ -185,6 +191,56 @@ func (g *App) buildUI() {
g.win.Resize(fyne.NewSize(380, 0))
}
// setupMonitorPicker populates the monitor dropdown and ties its visibility
// to the source selector. When monitor enumeration is unavailable (non-
// Hyprland), it falls back to a raw index entry driven by the saved config.
func (g *App) setupMonitorPicker() {
// Populate monitor names for the picker.
g.monitors, _ = capture.ListMonitors()
names := make([]string, 0, len(g.monitors))
for _, m := range g.monitors {
names = append(names, fmt.Sprintf("%s (%dx%d)", m.Name, m.Width, m.Height))
}
// If we could not enumerate, present the saved index as a single option.
if len(names) == 0 {
names = []string{fmt.Sprintf("Monitor %d", g.cfg.StreamIndex)}
g.monitors = []capture.Monitor{{Index: g.cfg.StreamIndex, Name: fmt.Sprintf("Monitor %d", g.cfg.StreamIndex)}}
}
g.monSel = widget.NewSelect(names, func(string) {})
if len(g.monitors) > 0 {
// Preselect the configured index if it's within range.
for i, m := range g.monitors {
if m.Index == g.cfg.StreamIndex {
g.monSel.SetSelectedIndex(i)
break
}
}
}
// Monitor picker only applies to the "screen" source.
g.srcSel.OnChanged = func(string) {
g.monSel.Disable()
if g.srcSel.Selected == "screen" {
g.monSel.Enable()
}
}
g.monSel.Disable()
if g.cfg.Source == "screen" {
g.monSel.Enable()
}
}
// selectedMonitorIndex returns the monitor index chosen in the picker, or the
// saved config value when the picker is unavailable/disabled.
func (g *App) selectedMonitorIndex() int {
if g.monSel != nil && g.monSel.SelectedIndex() >= 0 && g.monSel.SelectedIndex() < len(g.monitors) {
return g.monitors[g.monSel.SelectedIndex()].Index
}
return g.cfg.StreamIndex
}
// toggleStream starts or stops the engine based on current UI state.
func (g *App) toggleStream() {
if g.lock {
@@ -211,7 +267,7 @@ func (g *App) start() {
FPS: fps,
Source: g.srcSel.Selected,
Audio: g.audioChk.Checked,
StreamIndex: g.cfg.StreamIndex,
StreamIndex: g.selectedMonitorIndex(),
Announce: &announce,
}
if g.configPath != "" {
@@ -258,7 +314,16 @@ func (g *App) watchStats() {
// Fyne UI calls must run on the main thread.
fyne.Do(func() {
g.statusLab.SetText(formatStatus(st))
g.setTrayState(true, "TeleportFling · "+frames+" frames, "+dropped+" dropped")
if st.Err != nil {
g.statusLab.Importance = widget.DangerImportance
} else {
g.statusLab.Importance = widget.SuccessImportance
}
tip := "TeleportFling · " + frames + " frames, " + dropped + " dropped"
if st.Err != nil {
tip += " · error"
}
g.setTrayState(true, tip)
})
case <-done:
return
@@ -273,6 +338,9 @@ func formatStatus(st flinger.Status) string {
if st.Dropped > 0 {
base += " · " + itoa(int(st.Dropped)) + " dropped"
}
if st.Err != nil {
base += "\nError: " + st.Err.Error()
}
return base
}