first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package runtime
import "github.com/maximhq/vt10x"
// TabNoticeLevel controls how transient tab bar notices are styled.
type TabNoticeLevel int
const (
TabNoticeInfo TabNoticeLevel = iota
TabNoticeError
)
const hostTrackedVTModeMask = vt10x.ModeMouseMask | vt10x.ModeMouseSgr | vt10x.ModeFocus

View File

@@ -0,0 +1,94 @@
//go:build !windows
package runtime
import (
"context"
"io"
"os"
"os/signal"
"syscall"
"github.com/creack/pty"
"golang.org/x/term"
"os/exec"
)
// runWithPTY starts cmd attached to a new pseudo-terminal and relays I/O
// between the outer terminal and the PTY master. This lets TUI apps render
// correctly while bifrost retains control of the process.
func runWithPTY(ctx context.Context, stdout io.Writer, cmd *exec.Cmd) error {
// Get the initial terminal size from the real terminal
sz, err := pty.GetsizeFull(os.Stdin)
if err != nil {
// Fallback to a reasonable default if stdin isn't a terminal
sz = &pty.Winsize{Rows: 24, Cols: 80}
}
// Start the command with a PTY attached, sized to match the outer terminal
ptmx, err := pty.StartWithSize(cmd, sz)
if err != nil {
return err
}
defer ptmx.Close()
// Handle SIGWINCH — propagate terminal resizes to the PTY
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGWINCH)
done := make(chan struct{})
defer func() {
signal.Stop(sigCh)
close(done)
}()
go func() {
for {
select {
case <-sigCh:
if newSz, err := pty.GetsizeFull(os.Stdin); err == nil {
_ = pty.Setsize(ptmx, newSz)
}
case <-done:
return
}
}
}()
// Put the outer terminal into raw mode so keystrokes (Ctrl-C, etc.)
// are forwarded as bytes to the PTY rather than handled by the OS.
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
// If we can't go raw (e.g., piped input), continue without it
oldState = nil
}
if oldState != nil {
defer func() {
_ = term.Restore(int(os.Stdin.Fd()), oldState)
_, _ = io.WriteString(stdout, hostCursorResetSequence())
}()
}
// Relay stdout: PTY master → caller's stdout
// This goroutine exits when the child process dies and the PTY master
// returns EOF.
outDone := make(chan struct{})
go func() {
defer close(outDone)
_, _ = io.Copy(stdout, ptmx)
}()
// Relay stdin: outer terminal → PTY master
// This goroutine will block on os.Stdin.Read after the child exits;
// that's expected and harmless — it unblocks on the next keystroke.
go func() {
_, _ = io.Copy(ptmx, os.Stdin)
}()
// Wait for the command to finish
err = cmd.Wait()
// Drain any remaining PTY output
<-outDone
return err
}

View File

@@ -0,0 +1,20 @@
//go:build windows
package runtime
import (
"context"
"io"
"os"
"os/exec"
)
// runWithPTY is a no-op on Windows — falls back to direct stdin/stdout piping.
// Windows does not support POSIX pseudo-terminals; full support would require
// ConPTY which is not yet portable in Go.
func runWithPTY(_ context.Context, stdout io.Writer, cmd *exec.Cmd) error {
cmd.Stdout = stdout
cmd.Stderr = stdout
cmd.Stdin = os.Stdin
return cmd.Run()
}

View File

@@ -0,0 +1,240 @@
package runtime
import (
"bytes"
"encoding/json"
"os"
"strings"
"testing"
"github.com/maximhq/vt10x"
)
type replayFixture struct {
Cols int `json:"cols"`
Rows int `json:"rows"`
Chunks []string `json:"chunks"`
Snapshots []replaySnapshot `json:"snapshots"`
}
type replaySnapshot struct {
AfterChunk int `json:"after_chunk"`
Screen []string `json:"screen"`
}
func TestOpencodeScrollReplayFixture(t *testing.T) {
t.Parallel()
fixture := loadReplayFixture(t, "testdata/opencode_scroll_replay.json")
term := vt10x.New(vt10x.WithSize(fixture.Cols, fixture.Rows))
var normalizer vtStreamNormalizer
checkpoints := make(map[int][]string, len(fixture.Snapshots))
for _, snapshot := range fixture.Snapshots {
checkpoints[snapshot.AfterChunk] = snapshot.Screen
}
for i, chunk := range fixture.Chunks {
data := normalizer.Normalize([]byte(chunk))
if len(data) > 0 {
if _, err := term.Write(data); err != nil {
t.Fatalf("write chunk %d: %v", i+1, err)
}
}
if want, ok := checkpoints[i+1]; ok {
if got := snapshotScreen(term, fixture.Cols, fixture.Rows); !equalLines(got, want) {
t.Fatalf("snapshot after chunk %d mismatch\nwant:\n%s\n\ngot:\n%s", i+1, strings.Join(want, "\n"), strings.Join(got, "\n"))
}
}
}
}
func TestVTStreamNormalizerHandlesSplitTrueColorSGR(t *testing.T) {
t.Parallel()
term := vt10x.New(vt10x.WithSize(8, 1))
var normalizer vtStreamNormalizer
if data := normalizer.Normalize([]byte("\x1b[38:2::100")); len(data) != 0 {
t.Fatalf("expected incomplete chunk to be buffered, got %q", string(data))
}
data := normalizer.Normalize([]byte(":150:200mHi"))
if _, err := term.Write(data); err != nil {
t.Fatalf("write normalized chunk: %v", err)
}
term.Lock()
defer term.Unlock()
cell := term.Cell(0, 0)
if cell.Char != 'H' {
t.Fatalf("expected first cell to contain H, got %q", cell.Char)
}
if want := vt10x.Color(100<<16 | 150<<8 | 200); cell.FG != want {
t.Fatalf("expected truecolor fg %v, got %v", want, cell.FG)
}
}
func TestVTAlternateScreenRestore(t *testing.T) {
t.Parallel()
term := vt10x.New(vt10x.WithSize(10, 3))
var normalizer vtStreamNormalizer
writeReplayChunk(t, term, &normalizer, "main")
writeReplayChunk(t, term, &normalizer, "\x1b[?1049h\x1b[2J\x1b[Halt")
writeReplayChunk(t, term, &normalizer, "\x1b[?1049l")
got := snapshotScreen(term, 10, 3)
if got[0] != "main " {
t.Fatalf("expected main screen to be restored, got %q", got[0])
}
}
func TestVTWritesCursorPositionRepliesToConfiguredWriter(t *testing.T) {
t.Parallel()
var out bytes.Buffer
term := vt10x.New(
vt10x.WithWriter(&out),
vt10x.WithSize(10, 3),
)
if _, err := term.Write([]byte("\x1b[6n")); err != nil {
t.Fatalf("write cpr request: %v", err)
}
if got := out.String(); got != "\x1b[1;1R" {
t.Fatalf("expected CPR reply %q, got %q", "\x1b[1;1R", got)
}
}
func TestExtractCursorVisible(t *testing.T) {
t.Parallel()
if got := extractCursorVisible([]byte("hello")); got != -1 {
t.Fatalf("extractCursorVisible(no toggle) = %d, want -1", got)
}
if got := extractCursorVisible([]byte("\x1b[?25h")); got != 1 {
t.Fatalf("extractCursorVisible(show) = %d, want 1", got)
}
if got := extractCursorVisible([]byte("\x1b[?25l")); got != 0 {
t.Fatalf("extractCursorVisible(hide) = %d, want 0", got)
}
if got := extractCursorVisible([]byte("\x1b[?25h...\x1b[?25l")); got != 0 {
t.Fatalf("extractCursorVisible(last wins) = %d, want 0", got)
}
}
func TestNormalizerDropsKittyKeyboardProtocol(t *testing.T) {
t.Parallel()
term := vt10x.New(vt10x.WithSize(20, 5))
var normalizer vtStreamNormalizer
// Write text, move cursor to row 3, then send Kitty keyboard sequences.
// Without the fix, \x1b[>1u would be misinterpreted as DECRC (cursor
// restore) and snap the cursor back to (0,0).
writeReplayChunk(t, term, &normalizer, "hello") // cursor at (5, 0)
writeReplayChunk(t, term, &normalizer, "\x1b[4;1H") // move to row 4, col 1
writeReplayChunk(t, term, &normalizer, "world") // cursor at (5, 3)
writeReplayChunk(t, term, &normalizer, "\x1b[>1u") // push kitty keyboard mode
writeReplayChunk(t, term, &normalizer, "\x1b[?u") // query kitty keyboard mode
writeReplayChunk(t, term, &normalizer, "\x1b[<u") // pop kitty keyboard mode
writeReplayChunk(t, term, &normalizer, "\x1b[=1;2u") // kitty with flags
term.Lock()
cursor := term.Cursor()
term.Unlock()
// Cursor should still be at (5, 3) — kitty sequences must not move it.
if cursor.X != 5 || cursor.Y != 3 {
t.Fatalf("expected cursor at (5, 3), got (%d, %d) — kitty sequences corrupted cursor state", cursor.X, cursor.Y)
}
// Verify the text was written correctly.
got := snapshotScreen(term, 20, 5)
if !strings.HasPrefix(got[0], "hello") {
t.Fatalf("expected 'hello' on row 0, got %q", got[0])
}
if !strings.HasPrefix(got[3], "world") {
t.Fatalf("expected 'world' on row 3, got %q", got[3])
}
}
func TestNormalizerPassesThroughNormalCSI(t *testing.T) {
t.Parallel()
var normalizer vtStreamNormalizer
// Regular CSI sequences (\x1b[?1049h, \x1b[2J, \x1b[H) must pass through.
data := normalizer.Normalize([]byte("\x1b[?1049h\x1b[2J\x1b[H"))
if string(data) != "\x1b[?1049h\x1b[2J\x1b[H" {
t.Fatalf("expected normal CSI to pass through, got %q", string(data))
}
// Kitty keyboard protocol must be stripped.
data = normalizer.Normalize([]byte("A\x1b[>1uB"))
if string(data) != "AB" {
t.Fatalf("expected kitty sequence to be stripped, got %q", string(data))
}
}
func loadReplayFixture(t *testing.T, path string) replayFixture {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read replay fixture: %v", err)
}
var fixture replayFixture
if err := json.Unmarshal(data, &fixture); err != nil {
t.Fatalf("unmarshal replay fixture: %v", err)
}
return fixture
}
func writeReplayChunk(t *testing.T, term vt10x.Terminal, normalizer *vtStreamNormalizer, chunk string) {
t.Helper()
data := normalizer.Normalize([]byte(chunk))
if len(data) == 0 {
return
}
if _, err := term.Write(data); err != nil {
t.Fatalf("write replay chunk: %v", err)
}
}
func snapshotScreen(term vt10x.Terminal, cols, rows int) []string {
term.Lock()
defer term.Unlock()
lines := make([]string, rows)
for y := 0; y < rows; y++ {
var line []rune
for x := 0; x < cols; x++ {
ch := term.Cell(x, y).Char
if ch == 0 {
ch = ' '
}
line = append(line, ch)
}
lines[y] = string(line)
}
return lines
}
func equalLines(got, want []string) bool {
if len(got) != len(want) {
return false
}
for i := range got {
if got[i] != want[i] {
return false
}
}
return true
}

View File

@@ -0,0 +1,163 @@
package runtime
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/maximhq/bifrost/cli/internal/apis"
"github.com/maximhq/bifrost/cli/internal/harness"
)
// LaunchSpec holds the parameters needed to launch a harness subprocess.
type LaunchSpec struct {
Harness harness.Harness
BaseURL string
VirtualKey string
Model string
Worktree string // empty = no worktree, non-empty = worktree name (or " " for unnamed)
}
// BuildEnv constructs the environment variables for the harness process,
// including the provider endpoint, API key, and model overrides.
func BuildEnv(spec LaunchSpec) ([]string, error) {
endpoint, err := apis.BuildEndpoint(spec.BaseURL, spec.Harness.BasePath)
if err != nil {
return nil, err
}
env := os.Environ()
env = append(env, spec.Harness.BaseURLEnv+"="+endpoint)
vk := strings.TrimSpace(spec.VirtualKey)
if vk != "" {
env = append(env, spec.Harness.APIKeyEnv+"="+vk)
if spec.Harness.AuthTokenEnv != "" {
env = append(env, spec.Harness.AuthTokenEnv+"="+vk)
}
}
model := strings.TrimSpace(spec.Model)
if model != "" {
env = append(env, "BIFROST_MODEL="+model)
if spec.Harness.ModelEnv != "" {
env = append(env, spec.Harness.ModelEnv+"="+model)
}
}
// Mark session as running inside bifrost
env = append(env, "BIFROST_SESSION=1")
env = append(env, "BIFROST_BASE_URL="+spec.BaseURL)
return env, nil
}
// PreparedCmd holds a command ready to execute along with any cleanup
// function that should be called after the process exits.
type PreparedCmd struct {
Cmd *exec.Cmd
Cleanup func()
}
// PrepareCommand builds the exec.Cmd for a harness launch, including
// environment variables, pre-launch hooks, and CLI arguments.
func PrepareCommand(ctx context.Context, spec LaunchSpec) (*PreparedCmd, error) {
env, err := BuildEnv(spec)
if err != nil {
return nil, err
}
var cleanup func()
if spec.Harness.PreLaunch != nil {
endpoint, err := apis.BuildEndpoint(spec.BaseURL, spec.Harness.BasePath)
if err != nil {
return nil, fmt.Errorf("build endpoint for pre-launch: %w", err)
}
vk := strings.TrimSpace(spec.VirtualKey)
if vk == "" {
vk = "dummy-key"
}
extraEnv, c, err := spec.Harness.PreLaunch(endpoint, vk, spec.Model)
if err != nil {
return nil, fmt.Errorf("pre-launch %s: %w", spec.Harness.Label, err)
}
cleanup = c
env = append(env, extraEnv...)
}
args := []string{}
if spec.Harness.RunArgsForMod != nil {
args = append(args, spec.Harness.RunArgsForMod(spec.Model)...)
}
if spec.Worktree != "" && spec.Harness.WorktreeArgs != nil {
args = append(args, spec.Harness.WorktreeArgs(spec.Worktree)...)
}
cmd := exec.CommandContext(ctx, spec.Harness.Binary, args...)
cmd.Env = env
return &PreparedCmd{Cmd: cmd, Cleanup: cleanup}, nil
}
// RunInteractive launches the harness as an interactive subprocess with full
// TTY access. It prints a bifrost banner before launch and a summary after exit.
func RunInteractive(ctx context.Context, stdout, stderr io.Writer, spec LaunchSpec) error {
p, err := PrepareCommand(ctx, spec)
if err != nil {
return err
}
if p.Cleanup != nil {
defer p.Cleanup()
}
fmt.Fprint(stdout, renderBanner(spec))
if err := runWithPTY(ctx, stdout, p.Cmd); err != nil {
fmt.Fprintf(stdout, "\n\033[36mbifrost>\033[0m session ended with error: %v\n", err)
return fmt.Errorf("run harness: %w", err)
}
fmt.Fprintf(stdout, "\n\033[36mbifrost>\033[0m session ended\n")
return nil
}
// renderBanner builds the pre-launch info box showing harness, model,
// endpoint, and the equivalent command.
func renderBanner(spec LaunchSpec) string {
endpoint, err := apis.BuildEndpoint(spec.BaseURL, spec.Harness.BasePath)
if err != nil {
endpoint = spec.BaseURL + " (invalid)"
}
vkStatus := "no"
if strings.TrimSpace(spec.VirtualKey) != "" {
vkStatus = "yes"
}
cmdLine := spec.Harness.Binary
if spec.Harness.RunArgsForMod != nil {
if a := spec.Harness.RunArgsForMod(spec.Model); len(a) > 0 {
cmdLine += " " + strings.Join(a, " ")
}
}
if spec.Worktree != "" && spec.Harness.WorktreeArgs != nil {
if a := spec.Harness.WorktreeArgs(spec.Worktree); len(a) > 0 {
cmdLine += " " + strings.Join(a, " ")
}
}
cyan := "\033[36m"
dim := "\033[2m"
bold := "\033[1m"
reset := "\033[0m"
var b strings.Builder
b.WriteString("\n")
b.WriteString(dim + "───────────────────────────────────────────────────" + reset + "\n")
b.WriteString(cyan + "bifrost>" + reset + " " + bold + spec.Harness.Label + reset + " " + dim + spec.Model + reset + "\n")
b.WriteString(dim + " endpoint : " + reset + endpoint + "\n")
b.WriteString(dim + " vk : " + reset + vkStatus + "\n")
b.WriteString(dim + " command : " + reset + cmdLine + "\n")
b.WriteString(dim + "───────────────────────────────────────────────────" + reset + "\n")
b.WriteString("\n")
return b.String()
}

View File

@@ -0,0 +1,71 @@
package runtime
import (
"bytes"
"strconv"
)
// rewriteSGRParams converts colon-separated SGR sub-parameters to semicolon-
// separated equivalents. Each semicolon-delimited group is processed:
//
// - 4:x -> 4 (underline style -> basic underline)
// - 38:2:cs:r:g:b -> 38;2;r;g;b (fg true-color, drop colorspace)
// - 48:2:cs:r:g:b -> 48;2;r;g;b (bg true-color, drop colorspace)
// - 38:5:n -> 38;5;n (fg 256-color)
// - 48:5:n -> 48;5;n (bg 256-color)
// - 58:... -> (dropped - underline color, unsupported by vt10x)
// - other:x -> other (keep first sub-param only)
func rewriteSGRParams(params []byte) []byte {
parts := bytes.Split(params, []byte{';'})
var out [][]byte
for _, part := range parts {
if !bytes.ContainsRune(part, ':') {
out = append(out, part)
continue
}
subs := bytes.Split(part, []byte{':'})
if len(subs) == 0 {
continue
}
code, err := strconv.Atoi(string(subs[0]))
if err != nil {
out = append(out, subs[0])
continue
}
switch code {
case 4: // underline style -> basic underline
out = append(out, []byte("4"))
case 38, 48: // fg/bg color
if len(subs) >= 2 {
switch string(subs[1]) {
case "2": // true-color: code:2[:cs]:r:g:b
// Find r,g,b - skip optional colorspace id.
if len(subs) >= 6 {
// code:2:cs:r:g:b
out = append(out, subs[0], []byte("2"), subs[3], subs[4], subs[5])
} else if len(subs) >= 5 {
// code:2:r:g:b (no colorspace)
out = append(out, subs[0], []byte("2"), subs[2], subs[3], subs[4])
} else {
out = append(out, subs[0])
}
case "5": // 256-color: code:5:n
if len(subs) >= 3 {
out = append(out, subs[0], []byte("5"), subs[2])
} else {
out = append(out, subs[0])
}
default:
out = append(out, subs[0])
}
} else {
out = append(out, subs[0])
}
case 58: // underline color - not supported by vt10x, drop entirely
continue
default:
out = append(out, subs[0])
}
}
return bytes.Join(out, []byte{';'})
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
//go:build windows
package runtime
import (
"context"
"errors"
"io"
)
// ErrQuit is returned by RunTabbed when the user quits the chooser
// without creating any tabs.
var ErrQuit = errors.New("user quit")
// ErrBackToTabs is returned by the newTabFn when the user presses Ctrl+B
// to dismiss the chooser and return to tab command mode.
var ErrBackToTabs = errors.New("back to tabs")
// ErrUpdateRequested is returned by RunTabbed when the user presses U in
// tab command mode to trigger a self-update and re-exec.
var ErrUpdateRequested = errors.New("update requested")
// NewTabFunc is called when the user requests a new tab or reopens the
// chooser for the active tab.
// stdinReader provides keyboard input; when nil the callback should read os.Stdin.
// tabBarLine returns the current tab bar content for embedding in the chooser view.
type NewTabFunc func(ctx context.Context, notify func(level TabNoticeLevel, message string), tabBarLine func() string, stdinReader io.Reader, seed *LaunchSpec) (*LaunchSpec, error)
// RunTabbed is not supported on Windows — falls back to single-session mode.
func RunTabbed(ctx context.Context, stdout, stderr io.Writer, version string, updateVersion string, newTabFn NewTabFunc) error {
spec, err := newTabFn(ctx, func(TabNoticeLevel, string) {}, nil, nil, nil)
if err != nil {
return err
}
if spec == nil {
return ErrQuit
}
return RunInteractive(ctx, stdout, stderr, *spec)
}

View File

@@ -0,0 +1,40 @@
{
"cols": 18,
"rows": 6,
"chunks": [
"\u001b[?1049h\u001b[2J\u001b[H",
"HEADER ",
"\u001b[6;1HFOOTER ",
"\u001b[2;",
"5r\u001b[?6h\u001b[2;1H",
"item 1\r\nitem 2\r\n",
"item 3\r\nitem 4",
"\r\nitem 5",
"\r\nitem 6",
"\u001b[?6l\u001b[r"
],
"snapshots": [
{
"after_chunk": 3,
"screen": [
"HEADER ",
" ",
" ",
" ",
" ",
"FOOTER "
]
},
{
"after_chunk": 9,
"screen": [
"HEADER ",
"item 3 ",
"item 4 ",
"item 5 ",
"item 6 ",
"FOOTER "
]
}
]
}

View File

@@ -0,0 +1,161 @@
package runtime
import "bytes"
// vtStreamNormalizer preserves incomplete CSI sequences across PTY reads before
// applying the SGR compatibility rewrite that vt10x still needs.
type vtStreamNormalizer struct {
pendingCSI []byte
}
func (n *vtStreamNormalizer) Normalize(data []byte) []byte {
if len(n.pendingCSI) > 0 {
combined := make([]byte, 0, len(n.pendingCSI)+len(data))
combined = append(combined, n.pendingCSI...)
combined = append(combined, data...)
data = combined
n.pendingCSI = nil
}
if len(data) == 0 {
return nil
}
result := make([]byte, 0, len(data)+32)
for i := 0; i < len(data); {
if data[i] == 0x1b {
if i+1 >= len(data) {
n.pendingCSI = append(n.pendingCSI[:0], data[i:]...)
break
}
if data[i+1] == '[' {
start := i
j := i + 2
for j < len(data) && data[j] < 0x40 {
j++
}
if j >= len(data) {
n.pendingCSI = append(n.pendingCSI[:0], data[start:]...)
break
}
// Drop CSI sequences that vt10x would misinterpret.
// Sequences with intermediate bytes (>, <, =) are private-use
// extensions (e.g. Kitty keyboard \x1b[>1u) that vt10x's CSI
// parser misroutes. Also drop ?-prefixed sequences ending in
// 'u' (\x1b[?u — Kitty keyboard query) which vt10x wrongly
// dispatches as DECRC (cursor restore), corrupting cursor state.
if shouldDropCSI(data[i+2:j], data[j]) {
// silently drop — vt10x would misinterpret
} else if data[j] == 'm' && bytes.ContainsRune(data[i+2:j], ':') {
result = append(result, 0x1b, '[')
result = append(result, rewriteSGRParams(data[i+2:j])...)
result = append(result, 'm')
} else {
result = append(result, data[start:j+1]...)
}
i = j + 1
continue
}
}
result = append(result, data[i])
i++
}
return result
}
// extractCursorShape scans data for the last DECSCUSR sequence (\x1b[N SP q)
// and returns the cursor shape value (0-6). Returns -1 if none found.
// DECSCUSR: 0=default, 1=blinking block, 2=steady block, 3=blinking underline,
// 4=steady underline, 5=blinking bar, 6=steady bar.
func extractCursorShape(data []byte) int32 {
shape := int32(-1)
for i := 0; i < len(data); i++ {
if data[i] != 0x1b || i+1 >= len(data) || data[i+1] != '[' {
continue
}
j := i + 2
// Collect parameter + intermediate bytes (< 0x40)
for j < len(data) && data[j] < 0x40 {
j++
}
if j >= len(data) {
break
}
params := data[i+2 : j]
final := data[j]
// DECSCUSR: CSI Ps SP q — params end with space (0x20), final is 'q'
if final == 'q' && len(params) >= 2 && params[len(params)-1] == ' ' {
// Parse the digit(s) before the space
numPart := params[:len(params)-1]
if len(numPart) == 1 && numPart[0] >= '0' && numPart[0] <= '6' {
shape = int32(numPart[0] - '0')
}
}
i = j
}
return shape
}
// extractCursorVisible scans data for the last cursor visibility toggle
// (\x1b[?25h or \x1b[?25l). Returns 1 for show, 0 for hide, -1 if none found.
func extractCursorVisible(data []byte) int32 {
vis := int32(-1)
for i := 0; i+5 < len(data); i++ {
if data[i] != 0x1b || data[i+1] != '[' || data[i+2] != '?' ||
data[i+3] != '2' || data[i+4] != '5' {
continue
}
switch data[i+5] {
case 'h':
vis = 1
i += 5
case 'l':
vis = 0
i += 5
}
}
return vis
}
// lastCursorShowIndex returns the byte index of the last \x1b[?25h in data,
// or -1 if not found. Used to split vt10x writes so we can capture cursor
// position at the exact moment the child shows the cursor.
func lastCursorShowIndex(data []byte) int {
result := -1
for i := 0; i+5 < len(data); i++ {
if data[i] == 0x1b && data[i+1] == '[' && data[i+2] == '?' &&
data[i+3] == '2' && data[i+4] == '5' && data[i+5] == 'h' {
result = i
i += 5
}
}
return result
}
// shouldDropCSI decides whether a CSI sequence should be stripped before it
// reaches vt10x. Two categories are filtered:
//
// 1. Sequences with '>', '<', or '=' as the first parameter byte. These are
// private-use extensions (Kitty keyboard protocol, DA2 responses, etc.)
// that vt10x's CSI parser conflates with standard sequences.
//
// 2. '?'-prefixed sequences whose final byte is 'u'. The Kitty keyboard
// query (\x1b[?u) would otherwise be dispatched as DECRC (cursor restore)
// because vt10x's 'u' handler does not check the private flag.
//
// Regular '?'-prefixed sequences (\x1b[?1049h, \x1b[?25l, etc.) are NOT
// filtered — vt10x handles those correctly via its priv flag.
func shouldDropCSI(params []byte, finalByte byte) bool {
if len(params) == 0 {
return false
}
switch params[0] {
case '>', '<', '=':
return true
case '?':
return finalByte == 'u'
}
return false
}