102 lines
2.7 KiB
Go
102 lines
2.7 KiB
Go
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()
|
|
}
|