package fbdraw import ( "context" "fmt" "sync" "time" ) // Screen represents a single screen in the carousel type Screen struct { Name string Generator FrameGenerator ticker *time.Ticker stop chan struct{} } // Carousel manages rotating between multiple screens type Carousel struct { display FramebufferDisplay screens []*Screen currentIndex int rotationInterval time.Duration mu sync.Mutex ctx context.Context cancel context.CancelFunc wg sync.WaitGroup } // NewCarousel creates a new carousel func NewCarousel(display FramebufferDisplay, rotationInterval time.Duration) *Carousel { ctx, cancel := context.WithCancel(context.Background()) return &Carousel{ display: display, screens: make([]*Screen, 0), currentIndex: 0, rotationInterval: rotationInterval, ctx: ctx, cancel: cancel, } } // AddScreen adds a new screen to the carousel func (c *Carousel) AddScreen(name string, generator FrameGenerator) { c.mu.Lock() defer c.mu.Unlock() screen := &Screen{ Name: name, Generator: generator, stop: make(chan struct{}), } c.screens = append(c.screens, screen) } // Run starts the carousel func (c *Carousel) Run() error { if len(c.screens) == 0 { return fmt.Errorf("no screens added to carousel") } // Start rotation timer rotationTicker := time.NewTicker(c.rotationInterval) defer rotationTicker.Stop() // Start with first screen if err := c.activateScreen(0); err != nil { return err } // Main loop for { select { case <-c.ctx.Done(): return c.ctx.Err() case <-rotationTicker.C: // Move to next screen c.mu.Lock() nextIndex := (c.currentIndex + 1) % len(c.screens) c.mu.Unlock() if err := c.activateScreen(nextIndex); err != nil { return err } } } } // Stop stops the carousel func (c *Carousel) Stop() { c.cancel() c.wg.Wait() } // activateScreen switches to the specified screen func (c *Carousel) activateScreen(index int) error { c.mu.Lock() defer c.mu.Unlock() // Stop current screen if any if c.currentIndex >= 0 && c.currentIndex < len(c.screens) { close(c.screens[c.currentIndex].stop) if c.screens[c.currentIndex].ticker != nil { c.screens[c.currentIndex].ticker.Stop() } } // Wait for current screen to stop c.wg.Wait() // Start new screen c.currentIndex = index screen := c.screens[index] // Calculate frame interval fps := screen.Generator.FramesPerSecond() if fps <= 0 { fps = 1 // Default to 1 FPS if invalid } frameInterval := time.Duration(float64(time.Second) / fps) // Create new stop channel and ticker screen.stop = make(chan struct{}) screen.ticker = time.NewTicker(frameInterval) // Start frame generation goroutine c.wg.Add(1) go c.runScreen(screen) return nil } // runScreen runs a single screen's frame generation loop func (c *Carousel) runScreen(screen *Screen) { defer c.wg.Done() // Get display size width, height := c.display.Size() grid := NewCharGrid(width, height) // Generate first frame immediately if err := screen.Generator.GenerateFrame(grid); err == nil { _ = c.display.Write(grid) } // Frame generation loop for { select { case <-screen.stop: return case <-screen.ticker.C: if err := screen.Generator.GenerateFrame(grid); err == nil { _ = c.display.Write(grid) } } } }