Files
diple/terminal_cursor.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()
}