diff --git a/.gitignore b/.gitignore index 881a3ee..6d7b1f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ config.toml cookies.json dist/ .DS_Store +parasocial diff --git a/cmd/parasocial/main.go b/cmd/parasocial/main.go index 2557ec3..97ea30e 100644 --- a/cmd/parasocial/main.go +++ b/cmd/parasocial/main.go @@ -6,6 +6,7 @@ package main import ( "context" "errors" + "flag" "fmt" "os" "os/signal" @@ -16,10 +17,13 @@ import ( // main sets up process cancellation and reports fatal startup errors to stderr. func main() { + daemon := flag.Bool("daemon", false, "run as daemon without interactive TUI") + flag.Parse() + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - if err := app.Run(ctx); err != nil { + if err := app.Run(ctx, *daemon); err != nil { if errors.Is(err, context.Canceled) { return } diff --git a/internal/app/app.go b/internal/app/app.go index 53abbb1..0cf6ee6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -9,10 +9,13 @@ import ( ) // Run loads configuration and starts the terminal UI. -func Run(ctx context.Context) error { +func Run(ctx context.Context, daemon bool) error { cfg, err := config.LoadDefault() if err != nil { return err } + if daemon { + return tui.RunDaemon(ctx, tui.Options{Streamers: cfg.Streamers}) + } return tui.Run(ctx, tui.Options{Streamers: cfg.Streamers}) } diff --git a/internal/tui/daemon.go b/internal/tui/daemon.go new file mode 100644 index 0000000..88dc2dd --- /dev/null +++ b/internal/tui/daemon.go @@ -0,0 +1,75 @@ +package tui + +import ( + "context" + "fmt" + "strings" +) + +// RunDaemon starts the daemon mode which just logs everything instead of UI. +func RunDaemon(ctx context.Context, options Options) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + rt := newRuntime(ctx, options) + state := options.AuthState + if state == nil { + var err error + state, err = rt.reuseAuth() + if err != nil { + return err + } + } + + if state == nil { + fmt.Println("Starting authentication...") + authChan := make(chan AuthUpdate) + rt.startAuth(authChan) + + var authErr error + for authMsg := range authChan { + if authMsg.Line != "" { + fmt.Println(authMsg.Line) + } + if authMsg.Done { + if authMsg.Err != nil { + authErr = authMsg.Err + } else if authMsg.State != nil { + state = authMsg.State + } + break + } + } + + if authErr != nil { + return fmt.Errorf("authentication failed: %w", authErr) + } + fmt.Println("Authentication complete.") + } + + fmt.Println("Starting streamer resolution...") + streamerChan := make(chan StreamerUpdate) + rt.startResolve(state, streamerChan) + + for streamerMsg := range streamerChan { + if streamerMsg.Done { + break + } + if streamerMsg.Err != nil { + fmt.Printf("Error: %v\n", streamerMsg.Err) + } + if streamerMsg.IRC != nil { + if message, ok := formatIRCChatLine(streamerMsg.IRC.Line); ok { + fmt.Printf("[IRC] %s: %s\n", streamerMsg.IRC.Login, message) + } + } + if streamerMsg.Miner != nil { + line := strings.TrimSpace(streamerMsg.Miner.Line) + if line != "" { + fmt.Printf("[Miner] %s: %s\n", streamerMsg.Miner.Login, line) + } + } + } + + return nil +}