package main import ( "bytes" "io" "os" "sync" "github.com/charmbracelet/x/ansi" ) // terminalCursorOutput decorates Bubble Tea's completed frame writes with a // hardware cursor position. Bubble Tea otherwise parks the cursor at the // bottom of every frame, which prevents a real insertion caret inside a custom // editor. type terminalCursorOutput struct { file *os.File mu sync.Mutex visible bool column int row int } func newTerminalCursorOutput(file *os.File) *terminalCursorOutput { return &terminalCursorOutput{file: file} } func (o *terminalCursorOutput) SetCursor(visible bool, column, row int) { o.mu.Lock() defer o.mu.Unlock() o.visible, o.column, o.row = visible, column, row } func (o *terminalCursorOutput) FrameMarker() string { o.mu.Lock() defer o.mu.Unlock() if !o.visible { return "" } // This zero-width sequence makes frames at different insertion positions // distinct, preventing Bubble Tea from skipping a hardware-cursor-only // update. The output wrapper reasserts the same position after Bubble Tea // parks its cursor at the bottom of the frame. return ansi.CursorPosition(o.column, o.row) } func (o *terminalCursorOutput) Write(value []byte) (int, error) { o.mu.Lock() defer o.mu.Unlock() // Bubble Tea v1 can expose intermediate rows from an animated partial // repaint. This is especially visible when unchanged Markdown code blocks // below the changed rows contain dense ANSI styling. Terminals that support // synchronized output hold the completed frame until the reset sequence; // terminals that do not support it safely ignore both sequences. if !bytes.Equal(value, []byte(ansi.ShowCursor)) && !bytes.Equal(value, []byte(ansi.HideCursor)) { if _, err := io.WriteString(o.file, ansi.SetSynchronizedOutputMode); err != nil { return 0, err } defer func() { _, _ = io.WriteString(o.file, ansi.ResetSynchronizedOutputMode) }() } written, err := o.file.Write(value) if err != nil || written != len(value) { return written, err } // Let Bubble Tea restore the cursor normally during startup/shutdown. if bytes.Equal(value, []byte(ansi.ShowCursor)) || bytes.Equal(value, []byte(ansi.HideCursor)) { if bytes.Equal(value, []byte(ansi.ShowCursor)) { _, _ = io.WriteString(o.file, ansi.SetCursorStyle(0)) } return written, nil } if !o.visible { _, err = io.WriteString(o.file, ansi.HideCursor) return written, err } _, err = io.WriteString( o.file, ansi.SetCursorStyle(5)+ ansi.CursorPosition(o.column, o.row)+ ansi.ShowCursor, ) return written, err } func (o *terminalCursorOutput) Read(value []byte) (int, error) { return o.file.Read(value) } func (o *terminalCursorOutput) Close() error { return nil } func (o *terminalCursorOutput) Fd() uintptr { return o.file.Fd() }