feat: support custom --config path in CLI and GUI
Add config.LoadFrom/SaveTo for arbitrary paths and thread a --config flag through both entry points: - CLI: --config loads a file first; explicit flags override file values - GUI: --config selects the settings file used for load and save - tests for LoadFrom/SaveTo and default fallback
This commit is contained in:
@@ -3,12 +3,21 @@
|
|||||||
//
|
//
|
||||||
// The streaming engine lives in internal/flinger; this command only wires the
|
// The streaming engine lives in internal/flinger; this command only wires the
|
||||||
// GUI and tray around it. Headless use is handled by cmd/teleportfling.
|
// GUI and tray around it. Headless use is handled by cmd/teleportfling.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// teleportfling-gui [--config PATH]
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
"teleportfling/internal/gui"
|
"teleportfling/internal/gui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
gui.Run()
|
configPath := flag.String("config", "", "config file path (default: ~/.config/teleportfling/config.json)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
gui.Run(*configPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,15 @@
|
|||||||
// teleportfling [--name NAME] [--port PORT] [--quality 1..100]
|
// teleportfling [--name NAME] [--port PORT] [--quality 1..100]
|
||||||
// [--fps N] [--source screen|pattern] [--audio]
|
// [--fps N] [--source screen|pattern] [--audio]
|
||||||
// [--stream-index N] [--duration SECONDS]
|
// [--stream-index N] [--duration SECONDS]
|
||||||
|
// [--config PATH]
|
||||||
//
|
//
|
||||||
// --source pattern selects the M1 synthetic test pattern (colour bars with a
|
// --source pattern selects the M1 synthetic test pattern (colour bars with a
|
||||||
// moving box) instead of real screen capture, which is useful for testing
|
// moving box) instead of real screen capture, which is useful for testing
|
||||||
// without granting screen-share permission.
|
// without granting screen-share permission.
|
||||||
|
//
|
||||||
|
// --config loads a saved config file first; any flag given explicitly on the
|
||||||
|
// command line overrides the file value. When running under a service manager
|
||||||
|
// (e.g. a systemd user unit) use --config to point at the daemon's profile.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -28,6 +33,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"teleportfling/internal/config"
|
||||||
"teleportfling/internal/flinger"
|
"teleportfling/internal/flinger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,19 +48,33 @@ func main() {
|
|||||||
noAnnounce = flag.Bool("no-announce", false, "do not announce on the LAN (receiver must connect by IP)")
|
noAnnounce = flag.Bool("no-announce", false, "do not announce on the LAN (receiver must connect by IP)")
|
||||||
streamIndex = flag.Int("stream-index", 0, "monitor index to capture (screen source)")
|
streamIndex = flag.Int("stream-index", 0, "monitor index to capture (screen source)")
|
||||||
duration = flag.Duration("duration", 0, "stream duration (0 = run until interrupted)")
|
duration = flag.Duration("duration", 0, "stream duration (0 = run until interrupted)")
|
||||||
|
configPath = flag.String("config", "", "config file path (default: ~/.config/teleportfling/config.json)")
|
||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cfg := flinger.Config{
|
// Base config: loaded from file (or defaults when absent), then overridden
|
||||||
Name: *name,
|
// by any flag the user explicitly set.
|
||||||
Port: *port,
|
cfg := loadCLIConfig(*configPath)
|
||||||
Quality: *quality,
|
flag.Visit(func(f *flag.Flag) {
|
||||||
FPS: *fps,
|
switch f.Name {
|
||||||
Source: *source,
|
case "name":
|
||||||
Audio: *withAudio,
|
cfg.Name = *name
|
||||||
StreamIndex: *streamIndex,
|
case "port":
|
||||||
Announce: !*noAnnounce,
|
cfg.Port = *port
|
||||||
|
case "quality":
|
||||||
|
cfg.Quality = *quality
|
||||||
|
case "fps":
|
||||||
|
cfg.FPS = *fps
|
||||||
|
case "source":
|
||||||
|
cfg.Source = *source
|
||||||
|
case "audio":
|
||||||
|
cfg.Audio = *withAudio
|
||||||
|
case "no-announce":
|
||||||
|
cfg.Announce = !*noAnnounce
|
||||||
|
case "stream-index":
|
||||||
|
cfg.StreamIndex = *streamIndex
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
eng, err := flinger.New(cfg)
|
eng, err := flinger.New(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,3 +105,19 @@ func main() {
|
|||||||
eng.Stop()
|
eng.Stop()
|
||||||
log.Printf("teleportfling: stopped")
|
log.Printf("teleportfling: stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadCLIConfig returns the base flinger config. With a --config path it reads
|
||||||
|
// that file; otherwise it reads the default user config. Missing files fall
|
||||||
|
// back to defaults.
|
||||||
|
func loadCLIConfig(path string) flinger.Config {
|
||||||
|
p := path
|
||||||
|
if p == "" {
|
||||||
|
p = config.Path()
|
||||||
|
}
|
||||||
|
c, err := config.LoadFrom(p)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("teleportfling: config %s: %v (using defaults)", p, err)
|
||||||
|
return flinger.DefaultConfig()
|
||||||
|
}
|
||||||
|
return c.ToFlinger()
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,15 +71,20 @@ var pathVar = func() string {
|
|||||||
return filepath.Join(dir, "teleportfling", "config.json")
|
return filepath.Join(dir, "teleportfling", "config.json")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Path returns the config file location.
|
// Path returns the default config file location.
|
||||||
func Path() string {
|
func Path() string {
|
||||||
return pathVar
|
return pathVar
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads the config file, returning Default when it does not exist.
|
// Load reads the default config file, returning Default when it does not exist.
|
||||||
func Load() (Config, error) {
|
func Load() (Config, error) {
|
||||||
p := Path()
|
return LoadFrom(Path())
|
||||||
data, err := os.ReadFile(p)
|
}
|
||||||
|
|
||||||
|
// LoadFrom reads the config file at path, returning Default when it does not
|
||||||
|
// exist. This lets the CLI and GUI support custom --config paths.
|
||||||
|
func LoadFrom(path string) (Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
return Default(), nil
|
return Default(), nil
|
||||||
@@ -113,15 +118,19 @@ func Load() (Config, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save writes the config file, creating the directory if needed.
|
// Save writes the default config file, creating the directory if needed.
|
||||||
func Save(c Config) error {
|
func Save(c Config) error {
|
||||||
p := Path()
|
return SaveTo(Path(), c)
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
}
|
||||||
|
|
||||||
|
// SaveTo writes the config file at path, creating the directory if needed.
|
||||||
|
func SaveTo(path string, c Config) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data, err := json.MarshalIndent(c, "", " ")
|
data, err := json.MarshalIndent(c, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(p, data, 0o600)
|
return os.WriteFile(path, data, 0o600)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,3 +92,33 @@ func TestToFlingerAnnounceDefault(t *testing.T) {
|
|||||||
t.Error("ToFlinger announce default should be true")
|
t.Error("ToFlinger announce default should be true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLoadFromMissing returns defaults for a non-existent custom path.
|
||||||
|
func TestLoadFromMissing(t *testing.T) {
|
||||||
|
c, err := LoadFrom(filepath.Join(t.TempDir(), "missing", "config.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadFrom: %v", err)
|
||||||
|
}
|
||||||
|
if c.Port != 9756 {
|
||||||
|
t.Errorf("default port = %d, want 9756", c.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSaveToLoadFromRoundTrip uses an explicit path independent of pathVar.
|
||||||
|
func TestSaveToLoadFromRoundTrip(t *testing.T) {
|
||||||
|
p := filepath.Join(t.TempDir(), "custom", "daemon.json")
|
||||||
|
|
||||||
|
announce := false
|
||||||
|
want := Config{Name: "Daemon", Port: 9901, Quality: 90, FPS: 30, Source: "screen", Audio: true, Announce: &announce}
|
||||||
|
if err := SaveTo(p, want); err != nil {
|
||||||
|
t.Fatalf("SaveTo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := LoadFrom(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadFrom: %v", err)
|
||||||
|
}
|
||||||
|
if got.Name != want.Name || got.Port != want.Port || *got.Announce != false {
|
||||||
|
t.Errorf("round trip mismatch: got %+v want %+v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+25
-8
@@ -43,6 +43,9 @@ type App struct {
|
|||||||
win fyne.Window
|
win fyne.Window
|
||||||
desk desktop.App
|
desk desktop.App
|
||||||
|
|
||||||
|
// configPath overrides the default config location ("" = default).
|
||||||
|
configPath string
|
||||||
|
|
||||||
cfg config.Config
|
cfg config.Config
|
||||||
eng *flinger.Engine
|
eng *flinger.Engine
|
||||||
lock bool // serialises start/stop against UI actions
|
lock bool // serialises start/stop against UI actions
|
||||||
@@ -63,9 +66,10 @@ type App struct {
|
|||||||
statsDone chan struct{}
|
statsDone chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the GUI and blocks until the app exits.
|
// Run starts the GUI and blocks until the app exits. configPath selects a
|
||||||
func Run() {
|
// non-default settings file ("" uses the default location).
|
||||||
g := &App{}
|
func Run(configPath string) {
|
||||||
|
g := &App{configPath: configPath}
|
||||||
|
|
||||||
g.fyneApp = app.NewWithID(appID)
|
g.fyneApp = app.NewWithID(appID)
|
||||||
g.win = g.fyneApp.NewWindow("TeleportFling")
|
g.win = g.fyneApp.NewWindow("TeleportFling")
|
||||||
@@ -74,7 +78,7 @@ func Run() {
|
|||||||
g.iconActive = newTrayResource(color.NRGBA{R: 46, G: 125, B: 50, A: 255}, color.NRGBA{R: 150, G: 220, B: 140, A: 255})
|
g.iconActive = newTrayResource(color.NRGBA{R: 46, G: 125, B: 50, A: 255}, color.NRGBA{R: 150, G: 220, B: 140, A: 255})
|
||||||
|
|
||||||
// Load persisted settings (falling back to defaults).
|
// Load persisted settings (falling back to defaults).
|
||||||
g.cfg = mustLoadConfig()
|
g.cfg = g.mustLoadConfig()
|
||||||
|
|
||||||
g.buildUI()
|
g.buildUI()
|
||||||
|
|
||||||
@@ -89,9 +93,18 @@ func Run() {
|
|||||||
g.win.ShowAndRun()
|
g.win.ShowAndRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustLoadConfig loads the config, logging and falling back to defaults.
|
// mustLoadConfig loads the config (from the configured path, or the default),
|
||||||
func mustLoadConfig() config.Config {
|
// logging and falling back to defaults on error.
|
||||||
c, err := config.Load()
|
func (g *App) mustLoadConfig() config.Config {
|
||||||
|
var (
|
||||||
|
c config.Config
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if g.configPath != "" {
|
||||||
|
c, err = config.LoadFrom(g.configPath)
|
||||||
|
} else {
|
||||||
|
c, err = config.Load()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("gui: config load: %v (using defaults)", err)
|
log.Printf("gui: config load: %v (using defaults)", err)
|
||||||
return config.Default()
|
return config.Default()
|
||||||
@@ -201,7 +214,11 @@ func (g *App) start() {
|
|||||||
StreamIndex: g.cfg.StreamIndex,
|
StreamIndex: g.cfg.StreamIndex,
|
||||||
Announce: &announce,
|
Announce: &announce,
|
||||||
}
|
}
|
||||||
if err := config.Save(g.cfg); err != nil {
|
if g.configPath != "" {
|
||||||
|
if err := config.SaveTo(g.configPath, g.cfg); err != nil {
|
||||||
|
log.Printf("gui: config save: %v", err)
|
||||||
|
}
|
||||||
|
} else if err := config.Save(g.cfg); err != nil {
|
||||||
log.Printf("gui: config save: %v", err)
|
log.Printf("gui: config save: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user