feat: add daemon mode flag

Adds a `-daemon` flag to run the application in the background without
the interactive terminal UI. It outputs log lines to standard output
instead of utilizing `charmbracelet/bubbletea`.
This commit is contained in:
ruinivist
2026-05-31 23:15:17 +00:00
parent 0ae1b094c5
commit e8e6e55892
4 changed files with 85 additions and 2 deletions
+1
View File
@@ -2,3 +2,4 @@ config.toml
cookies.json
dist/
.DS_Store
parasocial
+5 -1
View File
@@ -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
}
+4 -1
View File
@@ -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})
}
+75
View File
@@ -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
}