import os import sys, time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from harness import Term import fakeapi srv, base = fakeapi.start() FAILS = [] VERBOSE = "-v" in sys.argv def check(name, cond, t=None): print(("PASS " if cond else "FAIL ") + name) if not cond: FAILS.append(name) if t: t.dump(name) def input_text(t): """Input box rows: the bar rows right above the mode line.""" L = t.lines() mode = [i for i, l in enumerate(L) if l.lstrip().startswith("┃ ") and ("Build ·" in l or "Plan ·" in l)] if not mode: return [] rows = [] i = mode[-1] - 1 while i >= 0: s = L[i].lstrip() if not s.startswith("┃"): break body = s[2:].split(" ")[0].rstrip() if s.startswith("┃ ") else "" if body == "" and rows: break if body: rows.append(body) i -= 1 rows.reverse() if rows and (rows[0].startswith("Ask anything") or rows[0].startswith("Enter queues")): return [] return rows def styled(t): """Screen text plus the reverse/bg attributes, to see highlight moves.""" out = [] for y in range(t.rows): row = t.screen.buffer[y] out.append("".join("%s%s%s" % (row[x].data, int(row[x].reverse), row[x].bg[:3]) for x in range(t.cols))) return out def clear(t): if [r for r in input_text(t) if r]: t.send("\x03", 0.2) # ctrl+c clears a non-empty input (twice on empty quits) def new(kitty=False, **kw): t = Term(kitty=kitty, args=("-b", base, "-k", "sk-test", "-m", "deepseek-flash"), **kw) return t def basic(t, form, tag): clear(t) t.send("hello world") for _ in range(5): t.arrow("left", form) t.send("X") check(f"{tag} left x5 insert", "hello Xworld" in input_text(t), t) t.arrow("right", form); t.arrow("right", form) t.send("Y") check(f"{tag} right x2 insert", "hello XwoYrld" in input_text(t), t) # boundaries for _ in range(30): t.arrow("left", form, t=0.02) t.pump(0.2) t.send("<") check(f"{tag} left past start", "") check(f"{tag} right past end", "" in input_text(t), t) clear(t) def cjk(t): clear(t) t.send("你好世界") y, x_end = t.cursor() t.arrow("left"); t.arrow("left") y2, x2 = t.cursor() check("cjk cursor moves 2 cells per char", x_end - x2 == 4, t) t.send("A") check("cjk insert between wide chars", "你好A世界" in input_text(t), t) t.arrow("right") t.send("B") check("cjk right over wide char", "你好A世B界" in input_text(t), t) clear(t) t.send("a😀b") t.arrow("left"); t.arrow("left") t.send("Z") check("emoji left", "aZ😀b" in input_text(t), t) clear(t) def multiline(t): clear(t) t.send("line one is long") t.send("\x0e") # ctrl+n newline t.send("l2") t.send("\x0e") t.send("third line here") rows = input_text(t) check("multiline 3 rows", rows[-3:] == ["line one is long", "l2", "third line here"], t) for _ in range(4): t.arrow("left") # cursor before "here" t.arrow("up") # col 11 -> clamp to end of "l2" t.send("U") rows = input_text(t) check("up clamps column to short line", rows[-2] == "l2U", t) t.arrow("up") # col 3 on line one t.send("V") rows = input_text(t) check("up keeps column", rows[-3] == "linVe one is long", t) t.arrow("down"); t.arrow("down") t.send("W") rows = input_text(t) check("down twice to last line", "W" in rows[-1], t) clear(t) def history(t): clear(t) t.send("first prompt\r", 1.5) t.send("second prompt\r", 1.5) t.arrow("up") check("hist up -> newest", input_text(t)[-1:] == ["second prompt"], t) t.arrow("up") check("hist up -> older", input_text(t)[-1:] == ["first prompt"], t) t.arrow("up") check("hist up at oldest stays", input_text(t)[-1:] == ["first prompt"], t) t.arrow("down") check("hist down -> newer", input_text(t)[-1:] == ["second prompt"], t) t.arrow("down") check("hist down past newest -> empty", input_text(t) in ([], [""]), t) # draft survives a trip through history t.send("my draft") t.arrow("up") t.arrow("down") check("draft restored after up/down", input_text(t)[-1:] == ["my draft"], t) clear(t) def wrapped(t): clear(t) long = " ".join("w%02d" % i for i in range(40)) # ~160 chars, wraps t.send(long, 0.4) nrows = len(input_text(t)) t.arrow("up") txt = "".join(input_text(t)) check("up on wrapped line keeps the text", "w39" in txt and "w00" in txt, t) t.send("Q") rows = input_text(t) check("up on wrapped line moves one visual row", nrows >= 2 and "Q" in "".join(rows[:-1]), t) clear(t) def completion(t): clear(t) t.send("/", 0.3) before = styled(t) t.arrow("down"); t.arrow("down") mid = styled(t) t.arrow("up") after = styled(t) check("completion popup reacts to down", before != mid, t) check("completion up differs from down-down", mid != after, t) check("completion arrows do not touch input", input_text(t)[-1:] == ["/"], t) t.send("\x1b", 0.3) clear(t) def modifiers(t): clear(t) t.send("alpha beta gamma") t.arrow("left", mod=5) # ctrl+left -> word left t.send("X") check("ctrl+left word", "alpha beta Xgamma" in input_text(t), t) t.arrow("left", mod=3) # alt+left t.arrow("left", mod=3) t.send("Y") check("alt+left word", "alpha Ybeta Xgamma" in input_text(t), t) t.arrow("right", mod=5) t.send("Z") check("ctrl+right word", "alpha YbetaZ Xgamma" in input_text(t), t) t.arrow("left", mod=2) # shift+left: should at least not insert junk t.send("S") txt = "".join(input_text(t)) check("shift+left no junk", "[" not in txt and ";" not in txt and "1;2" not in txt, t) clear(t) def split_and_burst(t, form): clear(t) t.send("abcdef") ss = "\x1bO" if form == "ss3" else "\x1b[" # escape sequence split across writes (slow link) import os os.write(t.fd, ss[:1].encode()); time.sleep(0.008) os.write(t.fd, (ss[1:] + "D").encode()); t.pump(0.2) t.send("1") check(f"split {form} seq", "abcde1f" in input_text(t), t) # key-repeat burst in one write t.send((ss + "D") * 4, 0.3) t.send("2") check(f"burst {form} x4", "ab2cde1f" in input_text(t), t) clear(t) def picker(t): clear(t) t.send("/model\r", 0.8) s0 = styled(t) t.arrow("down") s1 = styled(t) t.arrow("up") s2 = styled(t) check("picker down changes selection", s0 != s1, t) check("picker up returns", s0 == s2, t) t.send("\x1b", 0.4) clear(t) def run(kitty, tag): print("#####", tag) t = new(kitty=kitty) try: basic(t, None, tag + " native") basic(t, "csi" if t.appcursor() else "ss3", tag + " other-form") cjk(t) multiline(t) modifiers(t) split_and_burst(t, "ss3") split_and_burst(t, "csi") completion(t) wrapped(t) history(t) picker(t) finally: t.close() if __name__ == "__main__": run(False, "xterm") run(True, "kitty") for term in ("screen-256color", "tmux-256color", "linux", "vt220", "xterm"): print("#####", term) t = Term(args=("-b", base, "-k", "sk-test"), term=term) try: basic(t, "ss3", term + " ss3") basic(t, "csi", term + " csi") modifiers(t) multiline(t) cjk(t) finally: t.close() print("FAILED:", len(FAILS)) for f in FAILS: print(" -", f) sys.exit(min(len(FAILS), 100))