// Package sixel provides SIXEL graphics decoding, cell synchronization, // and terminal rendering for the Nebara OS TUI. package sixel import ( "bytes" _ "embed" "fmt" "image/png" "io" "os" "runtime" "sync" "github.com/gdamore/tcell/v2" gosixel "github.com/mattn/go-sixel" ) // DefaultLogoPNG contains the official Nebara OS PNG logo asset. // //go:embed logo.png var DefaultLogoPNG []byte const ( // DefaultLogoCellWidth is the number of character columns reserved for the logo. // 221px image width / ~8.5-9px cell width ≈ 25-26 columns. DefaultLogoCellWidth = 26 // DefaultLogoCellHeight is the number of character rows reserved for the logo. // 19px image height fits in 1 row (with breathing room from the spacer below). DefaultLogoCellHeight = 1 ) // LogoRenderer handles encoding and rendering of the SIXEL logo. type LogoRenderer struct { mu sync.Mutex sixelData []byte pixelWidth int pixelHeight int cellCols int cellRows int enabled bool lastX int lastY int hasDrawn bool needsDraw bool } // NewLogoRenderer initializes the renderer using the official PNG asset. func NewLogoRenderer() (*LogoRenderer, error) { return NewLogoRendererFromPNG(DefaultLogoPNG) } // NewLogoRendererFromPNG initializes the renderer from custom PNG bytes. func NewLogoRendererFromPNG(pngBytes []byte) (*LogoRenderer, error) { img, err := png.Decode(bytes.NewReader(pngBytes)) if err != nil { return nil, fmt.Errorf("decode logo PNG: %w", err) } bounds := img.Bounds() pw := bounds.Dx() ph := bounds.Dy() var buf bytes.Buffer enc := gosixel.NewEncoder(&buf) enc.Transparent = true if err := enc.Encode(img); err != nil { return nil, fmt.Errorf("encode SIXEL: %w", err) } return &LogoRenderer{ sixelData: buf.Bytes(), pixelWidth: pw, pixelHeight: ph, cellCols: DefaultLogoCellWidth, cellRows: DefaultLogoCellHeight, enabled: true, lastX: -1, lastY: -1, needsDraw: true, }, nil } // SetEnabled toggles SIXEL rendering. func (r *LogoRenderer) SetEnabled(enabled bool) { r.mu.Lock() defer r.mu.Unlock() r.enabled = enabled r.needsDraw = true } // IsEnabled returns whether SIXEL rendering is currently active. func (r *LogoRenderer) IsEnabled() bool { r.mu.Lock() defer r.mu.Unlock() return r.enabled } // Invalidate forces a re-render of the SIXEL logo on the next draw cycle. func (r *LogoRenderer) Invalidate() { r.mu.Lock() defer r.mu.Unlock() r.needsDraw = true } // CellCols returns the number of terminal character cells the logo occupies. func (r *LogoRenderer) CellCols() int { return r.cellCols } // CellRows returns the number of terminal character rows the logo occupies. func (r *LogoRenderer) CellRows() int { return r.cellRows } // SixelDataLength returns the byte size of the cached SIXEL stream. func (r *LogoRenderer) SixelDataLength() int { return len(r.sixelData) } // Draw locks the tcell screen buffer region and outputs the SIXEL sequence to the TTY. func (r *LogoRenderer) Draw(screen tcell.Screen, x, y int) error { r.mu.Lock() defer r.mu.Unlock() if !r.enabled || len(r.sixelData) == 0 { return nil } // 1. Lock the screen region in tcell buffer so tcell.Show() skips these cells. // This prevents tcell from clearing or drawing characters over the logo. screen.LockRegion(x, y, r.cellCols, r.cellRows, true) // Check if we need to re-emit the SIXEL data to the terminal. // We re-emit if position changed, or if explicitly invalidated (e.g. after resize/modal). posChanged := (x != r.lastX || y != r.lastY) if !r.hasDrawn || posChanged || r.needsDraw { if err := r.emitSixel(x, y); err != nil { return err } r.lastX = x r.lastY = y r.hasDrawn = true r.needsDraw = false } return nil } // emitSixel writes the escape sequences and SIXEL payload to the active terminal. func (r *LogoRenderer) emitSixel(x, y int) error { tty, err := openTTY() if err != nil { return fmt.Errorf("open TTY: %w", err) } defer tty.Close() // ANSI cursor positioning is 1-indexed: row = y + 1, col = x + 1. // Sequence: // \x1b[s : Save current cursor position // \x1b[%d;%dH: Move to target (row, col) // [SIXEL payload] // \x1b[u : Restore cursor position posSeq := fmt.Sprintf("\x1b[s\x1b[%d;%dH", y+1, x+1) if _, err := io.WriteString(tty, posSeq); err != nil { return err } if _, err := tty.Write(r.sixelData); err != nil { return err } if _, err := io.WriteString(tty, "\x1b[u"); err != nil { return err } return nil } // openTTY opens the active terminal output device directly, bypassing tcell text buffers. func openTTY() (io.WriteCloser, error) { if runtime.GOOS == "windows" { return os.OpenFile("CONOUT$", os.O_WRONLY, 0) } return os.OpenFile("/dev/tty", os.O_WRONLY, 0) }