python/tty/demo_tty.py · 14803 字节 · 下载原文
# demo_tty.py —— **第 2 层:彩色控制台**
# demo_tty.py — **Layer 2: color console**
#
# 这一层解决的问题是「**在一个字符界面上做版面**」:清屏、把光标挪到第几行第几列、
# The problem this layer solves is "**doing layout on a character screen**": clearing the screen, moving the cursor to row/column N,
# 设前景/背景色。老 DOS 程序(菜单、表格、状态栏)全靠这几个原语拼出来。
# setting foreground/background colors. Old DOS programs (menus, tables, status bars) were all built from these few primitives.
#
# Python 这一层**没有 conio/crt 绑定**(`Lib/python/gfx.py` 里那些 `*_print` 是文档性
# Python has **no conio/crt bindings** at this layer (the `*_print` entries in `Lib/python/gfx.py` are documentary
# 存根,`conio.vml` 的绑定也不是给 Python 调的),所以这里**直接输出 ANSI 转义序列**
# stubs, and the `conio.vml` bindings are not meant to be called from Python), so here we **emit ANSI escape sequences directly**
# —— 那正是"支持彩色的控制台程序":
# — which is exactly what a "color-capable console program" is:
#
# 清屏 ESC [ 2 J ESC [ H
# clear screen: ESC [ 2 J, ESC [ H
# 定位光标 ESC [ <行> ; <列> H
# position the cursor: ESC [ <row> ; <col> H
# 前景/背景 ESC [ 3x m / 4x m / 9x m / 10x m,256 色 38;5;N,真彩 38;2;r;g;b
# foreground/background: ESC [ 3x m / 4x m / 9x m / 10x m, 256-color 38;5;N, true color 38;2;r;g;b
# 复位 ESC [ 0 m
# reset: ESC [ 0 m
#
# ## ⚠ 本前端的坑:字符串字面量里**没有 `\xNN` 转义**
# ## ⚠ Pitfall of this frontend: **no `\xNN` escape** inside string literals
#
# `PythonCompiler/Lexer.cs` 的转义表只有 `\n \t \r \\ \' \"`,其余是
# the escape table in `PythonCompiler/Lexer.cs` only has `\n \t \r \\ \' \"`, everything else is
# 「丢掉反斜杠、留原字符」⇒ `"\x1b[31m"` 打出的是字面量 `x1b[31m`。
# "drop the backslash, keep the character" ⇒ `"\x1b[31m"` prints the literal `x1b[31m`.
# 所以 `ansi()` 用 `putchar(27)` **直接写 ESC 那个字节**,再把后半截当普通字符串打。
# So `ansi()` uses `putchar(27)` to **write the ESC byte directly**, then prints the rest as an ordinary string.
# `print_str`/`putchar` 都**不补换行**,换行要自己 `print_str("\n")`。
# Neither `print_str` nor `putchar` **appends a newline**; you have to emit it yourself with `print_str("\n")`.
# ↳ 2026-10-04 复验仍成立:`print_str("\x1b[31m")` 实测逐字节打出 `x1b[31m`(反斜杠被丢掉、留 `x`)。
# ↳ Still true, re-checked 2026-10-04: `print_str("\x1b[31m")` measures byte-for-byte `x1b[31m` (the backslash is dropped, `x` is kept).
#
# ## ~~⚠ 另一条踩过的坑:别在用户函数里连着调两次 `int_to_str`~~ ✅ **已修好(2026-10-04 复验)**
# ## ~~⚠ Another pitfall already hit: do not call `int_to_str` twice in a row inside a user function~~ ✅ **fixed (re-checked 2026-10-04)**
#
# 想「算出坐标再拼进转义串」曾会踩到 `Lib/` 那两套栈清理约定(见 CLAUDE.md ⑨):
# Wanting to "compute a coordinate and splice it into an escape string" used to run into the two stack-cleanup conventions in `Lib/` (see CLAUDE.md ⑨):
# 当时实测(留作历史)
# Measured at the time (kept as history):
#
# def at(row, col):
# print_str(int_to_str(row)); print_str(";"); print_str(int_to_str(col))
# at(1, 1) # 当时期望 1;1H,实得 1;22818H(第二个 int_to_str 读到的实参漂了)
# at(1, 1) # then expected 1;1H, actually got 1;22818H (the second int_to_str read a drifted argument)
#
# 2026-10-04 复验:那个形态现在**是对的** —— 同一复现 `at(1, 1)` 得 `1;1H`、`at(7, 42)` 得 `7;42H`,
# Re-checked 2026-10-04: that shape is **correct now** -- the same repro gives `1;1H` for `at(1, 1)` and `7;42H` for `at(7, 42)`,
# 顶层/单参/三参函数里连着调也都对(顶层 `str(1);str(2)` → `1;2`,三参 → `123`)。
# and consecutive calls at the top level and in 1-arg / 3-arg functions are all correct too (top level `str(1);str(2)` -> `1;2`, three args -> `123`).
# ⇒ 本 demo 的坐标仍**写成字面量**(`ansi("5;1H")`)只是因为它已经写好、跑通了;
# ⇒ This demo's coordinates are still **written as literals** (`ansi("5;1H")`) only because it is already written and working;
# 能跑就别动,新写的代码里 `int_to_str` 直接连着用即可。
# existing code is fine as is, and new code may call `int_to_str` back to back directly.
#
# ## 在哪儿能看到什么
# ## What you can see where
#
# · **真终端**(`vmlcli … | cat -v`):整套转义都生效。
# · **A real terminal** (`vmlcli … | cat -v`): the whole escape set works.
# · **手机"命令行"页**:那一层把 stdout 里的 ANSI 转成仓库统一的 `«»` 中间格式
# · **The phone's "command line" page**: that layer converts the ANSI in stdout into the repo's unified `«»` intermediate format
# (`UI/Shared/AnsiMarkup.cs`),**只认 SGR(颜色/样式)**;光标定位(`ESC[…H`)
# (`UI/Shared/AnsiMarkup.cs`), and it **only understands SGR (color/style)**; cursor positioning (`ESC[…H`)
# 与清屏(`ESC[2J`)会被**吃掉** —— 这是刻意的有限子集(见
# and screen clearing (`ESC[2J`) get **eaten** — this is a deliberately limited subset (see
# `Examples/c/ansi_colors.c` 的说明)。所以每行末尾都补了换行,
# the notes in `Examples/c/ansi_colors.c`). That is why every line ends with a newline:
# 定位被吃掉时输出仍然一行一句、读得通。
# even when positioning is eaten, the output still reads one sentence per line.
#
# 跑法:命令行页输入 vml run examples/python/demo_tty.py
# How to run: type this on the command line page vml run examples/python/demo_tty.py
# ── ANSI 小工具 ────────────────────────────────────────────────
# ── ANSI helpers ────────────────────────────────────────────────
# `ansi("31m")` = ESC[31m;定位也走同一个口子(`ansi("5;1H")` = ESC[5;1H)。
# `ansi("31m")` = ESC[31m; positioning goes through the same entry point (`ansi("5;1H")` = ESC[5;1H).
# ── 显式引用:下面用到的 `ui_*` 全部来自共享库 vmlui.vml ────────────
# ── explicit reference: every `ui_*` below comes from the shared library vmlui.vml ──
# `Lib/python/vml_ui.py` 是 GenLib 从 `Lib/shared/src/vmlui.c` 生成的声明文件(勿手写),
# `Lib/python/vml_ui.py` is the declaration file GenLib generates from `Lib/shared/src/vmlui.c` (do not hand-write);
# 前端读它登记名字与形参个数 —— 有了声明表,实参个数写错会在**编译期**报错。
# the frontend reads it to register the names and their arities — with that table a wrong argument count is a **compile-time** error.
import vml_ui
def ansi(code):
putchar(27)
print_str("[")
print_str(code)
def text(s):
print_str(s)
lang = ui_scr_get_language()
# ── 开场:清屏 + 回左上角 ───────────────────────────────────────
# ── opening: clear screen + back to the top-left ─────────────────────────
ansi("2J")
ansi("H")
# ── 标题条:蓝底白字(ESC[44;97m)──────────────────────────────
# ── title bar: white on blue (ESC[44;97m) ────────────────────────────
ansi("1;1H")
ansi("44;97m")
text(" demo_tty (Python) —— 彩色控制台 / ANSI 转义序列 " if lang == 0 else " demo_tty (Python) -- color console / ANSI escapes ")
ansi("0m")
text("\n")
ansi("2;1H")
ansi("90m")
text("清屏 ESC[2J 定位 ESC[r;cH 颜色 ESC[3xm / ESC[4xm 复位 ESC[0m" if lang == 0 else "clear ESC[2J cursor ESC[r;cH color ESC[3xm / ESC[4xm reset ESC[0m")
ansi("0m")
text("\n")
# ── 标准 8 色前景(30–37)──────────────────────────────────────
# ── standard 8 foreground colors (30–37) ────────────────────────────────
ansi("4;1H")
ansi("1;37m")
text("标准 8 色前景:" if lang == 0 else "Standard 8 foreground colors:")
ansi("0m")
text("\n")
ansi("5;1H")
ansi("30m")
text(" 30 黑 " if lang == 0 else " 30 black ")
ansi("0m")
ansi("31m")
text(" 31 红 " if lang == 0 else " 31 red ")
ansi("0m")
ansi("32m")
text(" 32 绿 " if lang == 0 else " 32 green ")
ansi("0m")
ansi("33m")
text(" 33 黄 " if lang == 0 else " 33 yellow ")
ansi("0m")
ansi("34m")
text(" 34 蓝 " if lang == 0 else " 34 blue ")
ansi("0m")
ansi("35m")
text(" 35 品红 " if lang == 0 else " 35 magenta ")
ansi("0m")
ansi("36m")
text(" 36 青 " if lang == 0 else " 36 cyan ")
ansi("0m")
ansi("37m")
text(" 37 白 " if lang == 0 else " 37 white ")
ansi("0m")
text("\n")
# ── 亮色前景(90–97)────────────────────────────────────────────
# ── bright foreground colors (90–97) ────────────────────────────────────
ansi("6;1H")
ansi("90m")
text(" 90 亮黑(灰) " if lang == 0 else " 90 gray ")
ansi("0m")
ansi("91m")
text(" 91 亮红 " if lang == 0 else " 91 bright red ")
ansi("0m")
ansi("92m")
text(" 92 亮绿 " if lang == 0 else " 92 bright green ")
ansi("0m")
ansi("93m")
text(" 93 亮黄 " if lang == 0 else " 93 bright yellow ")
ansi("0m")
ansi("94m")
text(" 94 亮蓝 " if lang == 0 else " 94 bright blue ")
ansi("0m")
ansi("95m")
text(" 95 亮品红 " if lang == 0 else " 95 bright magenta ")
ansi("0m")
ansi("96m")
text(" 96 亮青 " if lang == 0 else " 96 bright cyan ")
ansi("0m")
ansi("97m")
text(" 97 亮白 " if lang == 0 else " 97 bright white ")
ansi("0m")
text("\n")
# ── 背景色(40–47 / 100–107)───────────────────────────────────
# ── background colors (40–47 / 100–107) ─────────────────────────────────
ansi("8;1H")
ansi("1;37m")
text("背景色:" if lang == 0 else "Background colors:")
ansi("0m")
text("\n")
ansi("9;1H")
ansi("41m")
text(" 红底 " if lang == 0 else " red bg ")
ansi("0m")
ansi("42m")
text(" 绿底 " if lang == 0 else " green bg ")
ansi("0m")
ansi("44m")
text(" 蓝底 " if lang == 0 else " blue bg ")
ansi("0m")
ansi("46m")
text(" 青底 " if lang == 0 else " cyan bg ")
ansi("0m")
ansi("103m")
text(" 亮黄底 " if lang == 0 else " bright yellow bg ")
ansi("0m")
ansi("105m")
text(" 亮品红底 " if lang == 0 else " bright magenta bg ")
ansi("0m")
text("\n")
# ── 样式(1 粗 / 2 暗 / 3 斜 / 4 下划线 / 9 删除线)─────────────
# ── styles (1 bold / 2 dim / 3 italic / 4 underline / 9 strikethrough) ──────────
ansi("11;1H")
ansi("1;37m")
text("样式:" if lang == 0 else "Styles:")
ansi("0m")
ansi("1m")
text(" 粗体 " if lang == 0 else " bold ")
ansi("0m")
ansi("2m")
text(" 暗淡 " if lang == 0 else " dim ")
ansi("0m")
ansi("3m")
text(" 斜体 " if lang == 0 else " italic ")
ansi("0m")
ansi("4m")
text(" 下划线 " if lang == 0 else " underline ")
ansi("0m")
ansi("9m")
text(" 删除线 " if lang == 0 else " strike ")
ansi("0m")
text("\n")
# ── 256 色(38;5;N)与真彩(38;2;r;g;b)─────────────────────────
# ── 256 colors (38;5;N) and true color (38;2;r;g;b) ─────────────────────
ansi("13;1H")
ansi("1;37m")
text("256 色 / 真彩:" if lang == 0 else "256 colors / truecolor:")
ansi("0m")
ansi("38;5;208m")
text(" 256-208 橙 " if lang == 0 else " 256-208 orange ")
ansi("0m")
ansi("38;5;46m")
text(" 256-46 亮绿 " if lang == 0 else " 256-46 bright green ")
ansi("0m")
ansi("38;2;255;128;0m")
text(" 真彩橙 " if lang == 0 else " truecolor orange ")
ansi("0m")
ansi("48;2;60;0;90m")
text(" 真彩深紫底 " if lang == 0 else " truecolor deep purple bg ")
ansi("0m")
text("\n")
# ── 循环画一条色带(12 格,颜色在 8 档里循环)──────────────────
# ── draw a color band in a loop (12 cells, colors cycling through 8 steps) ───────────────
# 「算出来的」那一格:用取模 + 分支挑出字面量色码。这不是绕远 ——
# The "computed" cell: pick a literal color code with modulo + branches. This is not a detour —
# 数字转字符串这条路在多数前端上要么缺、要么像上面那样会漂,
# the number-to-string path is either missing on most frontends or drifts as shown above,
# 所以「色码写字面量、循环只算用哪一个」是所有语言都能照做的写法。
# so "write the color codes as literals, let the loop only compute which one to use" is a pattern every language can follow.
ansi("16;1H")
ansi("1;37m")
text("循环画 12 格色带:" if lang == 0 else "Draw a 12-cell color band in a loop:")
ansi("0m")
text("\n")
ansi("17;1H")
i = 0
while i < 12:
k = i % 8
if k == 0:
ansi("41m")
if k == 1:
ansi("42m")
if k == 2:
ansi("43m")
if k == 3:
ansi("44m")
if k == 4:
ansi("45m")
if k == 5:
ansi("46m")
if k == 6:
ansi("47m")
if k == 7:
ansi("100m")
text(" ")
ansi("0m")
i = i + 1
text("\n")
# ── 收尾:复位颜色 + 定位到第 20 行写结束语 ─────────────────────
# ── wrap-up: reset the colors + position to row 20 and write the closing line ────────────
ansi("0m")
ansi("20;1H")
ansi("1;32m")
text("demo_tty 结束 —— 没有按键等待,画完即退出。" if lang == 0 else "demo_tty done -- no key wait, draws and exits.")
ansi("0m")
text("\n")