TUI keys: register both CSI and SS3 forms of the cursor/Home/End keys and
the xterm modifier sequences when terminfo lacks them, so an arrow sent in
the "other" form no longer turns into Esc + a typed letter. Up/Down move by
visual (soft-wrapped) row, and the draft survives a trip through history.
scripts/tui_keys drives the TUI in a pty to check all of this.
Images: text inside a multimodal content array is now a {"type":"text"}
part; DeepSeek rejected the bare string with HTTP 422. deepseek-flash
(and its deepseek-chat / deepseek-reasoner aliases) read images, so the
registry marks them vision; deepseek-v4-pro stays text only.
CI: models.c lost _DEFAULT_SOURCE in 0.7.0 and gmtime_r broke the musl
build; tests sleep with poll() instead of usleep for the same reason.
100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
"""Drive the cst-agent TUI in a pty and read the screen back (pyte).
|
|
|
|
Run from the repo root after `make`:
|
|
nix-shell -p python3Packages.pyte --run "python3 scripts/tui_keys/keys.py"
|
|
nix-shell -p python3Packages.pyte --run "python3 scripts/tui_keys/esc.py"
|
|
(or `pip install pyte`). A local fake OpenAI-compatible server answers the
|
|
model calls, so no API key is needed. Exit status is the failure count.
|
|
"""
|
|
import os, pty, sys, time, select, fcntl, termios, struct, signal, shutil, tempfile
|
|
import pyte
|
|
|
|
BIN = os.environ.get("CST_BIN") or os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), "..", "..", "cst-agent")
|
|
|
|
|
|
class Term:
|
|
def __init__(self, cols=120, rows=36, kitty=False, env=None, args=(), home=None,
|
|
ws=None, term="xterm-256color"):
|
|
self.cols, self.rows, self.kitty = cols, rows, kitty
|
|
self.screen = pyte.Screen(cols, rows)
|
|
self.stream = pyte.ByteStream(self.screen)
|
|
self.home = home or tempfile.mkdtemp(prefix="cstk_home_")
|
|
self.ws = ws or tempfile.mkdtemp(prefix="cstk_ws_")
|
|
e = dict(os.environ)
|
|
e.update({"TERM": term, "HOME": self.home,
|
|
"XDG_CONFIG_HOME": os.path.join(self.home, ".config"),
|
|
"LANG": "C.UTF-8", "LC_ALL": "C.UTF-8",
|
|
"CST_PROVIDER": "deepseek", "DEEPSEEK_API_KEY": "sk-dummy-test",
|
|
"CST_METRICS": "0"})
|
|
e.pop("NO_COLOR", None)
|
|
if env:
|
|
e.update(env)
|
|
self.raw = b""
|
|
pid, fd = pty.fork()
|
|
if pid == 0:
|
|
os.chdir(self.ws)
|
|
os.execve(BIN, [BIN, "-N", *args], e)
|
|
self.pid, self.fd = pid, fd
|
|
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
|
|
self.pump(1.5)
|
|
|
|
def pump(self, t=0.25):
|
|
end = time.time() + t
|
|
while True:
|
|
left = end - time.time()
|
|
if left <= 0:
|
|
break
|
|
r, _, _ = select.select([self.fd], [], [], left)
|
|
if not r:
|
|
continue
|
|
try:
|
|
d = os.read(self.fd, 65536)
|
|
except OSError:
|
|
break
|
|
if not d:
|
|
break
|
|
self.raw += d
|
|
if self.kitty and b"\x1b[?u" in d:
|
|
os.write(self.fd, b"\x1b[?0u")
|
|
self.stream.feed(d)
|
|
|
|
def send(self, b, t=0.15):
|
|
if isinstance(b, str):
|
|
b = b.encode()
|
|
os.write(self.fd, b)
|
|
self.pump(t)
|
|
|
|
def appcursor(self):
|
|
return (1 << 5) in self.screen.mode # pyte DECCKM
|
|
|
|
def arrow(self, d, form=None, mod=None, t=0.15):
|
|
c = {"up": "A", "down": "B", "right": "C", "left": "D"}[d]
|
|
if mod:
|
|
seq = "\x1b[1;%d%s" % (mod, c)
|
|
else:
|
|
f = form or ("ss3" if self.appcursor() else "csi")
|
|
seq = ("\x1bO" if f == "ss3" else "\x1b[") + c
|
|
self.send(seq, t)
|
|
|
|
def lines(self):
|
|
return [l.rstrip() for l in self.screen.display]
|
|
|
|
def find(self, s):
|
|
return [(i, l) for i, l in enumerate(self.lines()) if s in l]
|
|
|
|
def cursor(self):
|
|
return self.screen.cursor.y, self.screen.cursor.x
|
|
|
|
def dump(self, title=""):
|
|
print("=" * 20, title, "cursor", self.cursor())
|
|
for i, l in enumerate(self.lines()):
|
|
print("%2d|%s" % (i, l))
|
|
|
|
def close(self):
|
|
try:
|
|
os.kill(self.pid, signal.SIGTERM)
|
|
os.waitpid(self.pid, 0)
|
|
except Exception:
|
|
pass
|