# stock.py —— 常用小程序③：**股票行情**（联网抓 A 股行情 → 彩色卡片；抓不到就回落读本地）
# stock.py -- Everyday utility #3: **stock quotes** (fetch A-share quotes over the network, draw
# them as colour cards, and fall back to a local file when the fetch fails)
#
# 干什么：一屏卡片，每只股票一张 —— 代码、名称、现价、涨跌幅（中国习惯：涨红跌绿），
# What it does: one full screen of cards, one per stock -- code, name, last price, change percent
# (red for up, green for down, the Chinese convention), plus a small bar drawn from the change.
# 数据从哪来：`qt.gtimg.cn` 的**明文 HTTP**（80 端口，免密钥）。
# Where the data comes from: plain HTTP (port 80, no API key) against `qt.gtimg.cn`.
# 响应是一行 `v_<代码>="0~1~2~…";`，**记录之间用 `;` 分、字段之间用 `~` 分**。
# The reply is a line of `v_<code>="0~1~2~...";`, **records separated by `;` and fields by `~`**.
# 离线怎么回落：任一连网环节失败就去读同目录的 `stocks.txt`（每行 `代码|名称|现价|涨跌幅`，
# Offline fallback: when any networking step fails it reads `stocks.txt` next to the program (one
# `code|name|price|change%` per line, `#` starts a comment, UTF-8), and the title says so.
# `#` 开头是注释，UTF-8），标题上标一行"离线：读本地 stocks.txt"。
# This is the Python port of `Examples/c/app/stock.c`: the layout, the field indices, the colour
# 这是 `Examples/c/app/stock.c` 的 Python 移植：版式、字段下标、配色、回落候选表都照搬。
# scheme and the fallback candidate list are all carried over.
#
# ## Python 前端没有 `asm()`、没有 `peek`/`poke`（**都实测过**）
# ## No `asm()`, no `peek`/`poke` in this front end (both **measured**)
#
# 套接字/文件走共享库的 `net_*`、`vml_dns_resolve`、`vml_file_*`；读内存走 `str_charat`（读一个
# Sockets/files go through the shared library's `net_*`, `vml_dns_resolve`, `vml_file_*`; memory is
# 字节）、`memset(addr + i, byte, 1)`（写一个字节）、`str_substr`（取一段）。
# read with `str_charat` (one byte), written with `memset(addr + i, byte, 1)` (one byte) and sliced
# with `str_substr`.
# ⚠ `str_charat` 回的是**有符号**字节（实测 `0xE5` 回 -27）⇒ 凡是要与 `0x7F`/`0x81`/`0xA1` 比大小
# ⚠ `str_charat` returns a **signed** byte (measured: `0xE5` comes back as -27), so every byte
#   compared against `0x7F`/`0x81`/`0xA1` is masked with `& 255` first (the `& 255` below).
#   的地方一律先 `& 255`（见下面各处 `& 255`）。ASCII 的比较不受影响。
#   ASCII comparisons are unaffected.
#
# ## 名称是 **GBK**，而 `encoding_gbk_decode` 是**错的**（实测）
# ## The names arrive as **GBK**, and `encoding_gbk_decode` is **wrong** (measured)
#
# 那个库函数不是查表而是近似式：`0x554A + (b0 - 0xB0) * 94 + (b1 - 0xA1)`，实测 `浦`（GBK 字节
# That library function is an approximation rather than a table: `0x554A + (b0-0xB0)*94 + (b1-0xA1)`.
# `C6 D6`，本该是 U+6D66）它给 U+5D93 —— 画出来是**另一个汉字**，不是乱码，最难认。
# Measured: the GBK bytes `C6 D6` come back as U+5D93 -- **a different hanzi**,
# not mojibake, which is the hardest kind to notice.
# ⇒ 本文件自带一张 **GB2312 全表**（6768 项，见 `load_gtab`），只借编码库里**确实正确**的那一半
# => This file carries its own **GB2312 table** (6768 entries, see `load_gtab`) and borrows only the
#    half of the encoding library that is **actually correct** (the UTF-8 one) -- here that is not
#    even needed: the table is *stored* as UTF-8 bytes, so a lookup is a plain slice.
#   （UTF-8 那一半）—— 本文件连那半都省了：表**按 UTF-8 存放**，查表就是切一段。
#   half -- and here even that is unnecessary: the table *is* UTF-8, so a lookup is a slice.
# ⚠ 表里查不到的槽位（GB2312 未定义的 5 个）在 C 版是半角 `?`，本版**补成 3 字节的全角 `？`**，
# ⚠ The 5 slots GB2312 leaves undefined hold a half-width `?` in the C version; here they are a
#   三字节的全角问号 —— 这样**每项正好 3 字节**，`下标 × 3` 就是字节偏移，不必再带一张偏移表。
#   3-byte full-width `？`, which makes **every entry exactly 3 bytes** so `index * 3` *is* the byte
#   offset and no separate offset table is needed. Only GBK extensions (outside GB2312) can reach
#   ⚠ 只有落在 GB2312 之外的 GBK 扩展字才可能碰到它，那种字 C 版本来也画成 `?`。
#   those slots, and the C version draws those as `?` anyway.
#
# ## 版面：所有尺寸都从宿主问，颜色一律真彩 `0xAARRGGBB`，画完必须 `ui_2d_present()`
# ## Layout: every dimension is asked of the host, colours are true colour, and the frame must end
#    with `ui_2d_present()`
# 循环**必须有界**（例程要能被自动验收）：空闲 8 秒自己退出，`Esc`/`q` 也退出；其余任意键 =
# The loop **must be bounded** (samples have to be automatically verifiable): it exits after 8 s of
# idling, `Esc`/`q` also quits, and any other key re-fetches and redraws.
# 重新抓一次并刷新。
# and any other key re-fetches and redraws.
# ⚠ 五个**实测**出来的坑照抄在注释里，改本文件前先读 `stock.c` 的文件头。
# ⚠ The five **measured** pitfalls are reproduced in the C header; read it before changing this.

# ── 显式引用：`ui_*` 来自共享调用库 `vmlui.vml`；套接字/文件/字符串助手来自共享库 ──
# ── explicit references: `ui_*` from the shared call library `vmlui.vml`; the socket/file/string
#    helpers from the shared library ──
import vml_ui
import shared

HOST = "qt.gtimg.cn"
PORT = 80

WANT = 6                  # 请求几只
# how many stocks to ask for
MAXSTK = 8                # 扁平缓冲能装几只
# how many the flat buffers hold
BUFCAP = 8192             # 响应缓冲：6 只实测 3175 字节
# response buffer: 3175 bytes for six
MAXROUNDS = 64            # 读循环上限，防"对端不关连接"时死读
# read-loop cap
IDLE_EXIT_MS = 8000

# 请求的 6 只（沪 `sh` + 代码、深 `sz` + 代码）。特意**涨跌都有**，好一眼看出红绿两种条。
# The six requested stocks (`sh`/`sz` plus the code). Deliberately **a mix of risers and fallers**,
# so both the red and the green bar show up at a glance.
QUERY = "sh600000,sz000001,sh601318,sh600519,sz000333,sh688981"

# 配色（真彩 AA RRGGBB）
# Colours (true colour AA RRGGBB)
C_BG = 0xFF0E1116
C_CARD = 0xFF1A1F28
C_TRACK = 0xFF2A3038
C_TITLE = 0xFFE8E8F0
C_DIM = 0xFF9AA0B0
C_UP = 0xFFE05A4A          # 涨 = 红（中国习惯）
# up = red (the Chinese convention)
C_DOWN = 0xFF3FAE6A        # 跌 = 绿
# down = green
C_WARN = 0xFFE0A040        # 离线提示
# the offline notice

# 请求正文。C 版一段一段 `strcat` 拼，本语言的字符串字面量直接支持；**必须写在一行**，
# The request body. The C version stitches it with `strcat`; here one string literal does it.
# 本语言不做相邻字面量拼接（实测 `"x" "y"` 只编出 `"x"`）。
# It **must stay on one line**: adjacent literals are not concatenated here (measured).
# ⚠ HTTP/1.0 + `Connection: close` 是关键：用 HTTP/1.1 问会回 `Transfer-Encoding: chunked` 且
# ⚠ HTTP/1.0 plus `Connection: close` is the crux: HTTP/1.1 gets a chunked body and a connection
#   不关连接，读循环收不了尾（实测 3414 字节后 recv 才回 0）。
#   that never closes, leaving the read loop with no end (measured: recv returns 0 after 3414 B).
PATH = "/q=sh600000,sz000001,sh601318,sh600519,sz000333,sh688981"

# ── 缓冲与状态 ────────────────────────────────────────
# ── buffers and state ─────────────────────────────────
RBUF = vml_alloc(BUFCAP)        # 整个 HTTP 响应（头 + 正文），离线时当正文用
                                # The whole HTTP response; used as the body when offline
GT = vml_alloc(20500)           # GB2312 全表（UTF-8，每项 3 字节 ⇒ 下标 × 3 = 字节偏移）
                                # The GB2312 table as UTF-8, 3 bytes per entry
S_NUM = vml_alloc(16)           # 整数转字符串的落点
# where `itoa` writes
S_PB = vml_alloc(24)            # "+3.27%" 这种涨跌幅串
# the "+3.27%" change text

# 一组扁平缓冲：每只股票定长一格（C 版是定长数组，本语言直接 `vml_alloc` 一块同形状的内存，
# One flat buffer set, one fixed-size cell per stock (the C version uses fixed arrays; here the same
# 读写都按地址 —— 恰好就是 C 版那几句 `s_name + i * 32`）。
# shape comes from `vml_alloc` and every access is by address, exactly the C's `s_name + i * 32`).
S_CODE = vml_alloc(MAXSTK * 12)     # 代码（ASCII）
# code (ASCII)
S_NAME = vml_alloc(MAXSTK * 32)     # 名称（GBK 转成的 UTF-8）
# name (GBK converted to UTF-8)
S_PRICE = vml_alloc(MAXSTK * 16)    # 现价原文
# last price, as text
S_PCT = vml_alloc(MAXSTK * 16)      # 涨跌幅原文（带符号）
# change percent, as text (signed)

# 画布尺寸 / 界面语言 / 内存里的字节数 / 正文起点 / 正文长度 / 解析右边界 / 离线标志 / 实际只数
# Canvas size / UI language / bytes in RBUF / body offset / body length / right edge / offline flag
# / how many stocks were parsed
SW = 0
SH = 0
LANG = 0
TOTAL = 0
HEAD = 0
BLEN = 0
LIM = 0
OFFLINE = 0
N_STK = 0
FIRST = 1

# 涨跌幅 × 100（画条用）。C 版是定长数组，本语言列表在函数里**可写**（实测），所以在
# Change percent x 100 (for the bar). C uses a fixed array; a list here is **writable inside a
# `parse_quote`/`parse_local_line` 里按下标写、`draw` 里读。
# function** (measured), so the parsers write it by index and `draw` reads it.
S_PCT100 = [0, 0, 0, 0, 0, 0, 0, 0]


# ── GB2312 表（**数据，不是逻辑**；一行一个"区"，区号 0xB0..0xF7 共 72 行 6768 项）──
# ── The GB2312 table (**data, not logic**); one zone per line, 0xB0..0xF7, 72 lines, 6768 entries ──
# 下标 = (b0 - 0xB0) * 94 + (b1 - 0xA1)，查出来是**这个字的 UTF-8 三字节**。
# The index is (b0 - 0xB0) * 94 + (b1 - 0xA1) and the value is that character's **three UTF-8 bytes**.
# ⚠ 每行 94 个字符（C 版就是这一份），`strcat` 一句一句接起来 —— 本语言**不做相邻字面量拼接**，
# ⚠ Each line is 94 characters (the very table the C version carries); the lines are joined with
#   所以 C 版那种"一行一个区、跨行连着写"在这里要显式 `strcat`。
#   `strcat` because adjacent string literals are **not** concatenated in this language.
def load_gtab():
    strcpy(GT, "啊阿埃挨哎唉哀皑癌蔼矮艾碍爱隘鞍氨安俺按暗岸胺案肮昂盎凹敖熬翱袄傲奥懊澳芭捌扒叭吧笆八疤巴拔跋靶把耙坝霸罢爸白柏百摆佰败拜稗斑班搬扳般颁板版扮拌伴瓣半办绊邦帮梆榜膀绑棒磅蚌镑傍谤苞胞包褒剥")
    strcat(GT, "薄雹保堡饱宝抱报暴豹鲍爆杯碑悲卑北辈背贝钡倍狈备惫焙被奔苯本笨崩绷甭泵蹦迸逼鼻比鄙笔彼碧蓖蔽毕毙毖币庇痹闭敝弊必辟壁臂避陛鞭边编贬扁便变卞辨辩辫遍标彪膘表鳖憋别瘪彬斌濒滨宾摈兵冰柄丙秉饼炳")
    strcat(GT, "病并玻菠播拨钵波博勃搏铂箔伯帛舶脖膊渤泊驳捕卜哺补埠不布步簿部怖擦猜裁材才财睬踩采彩菜蔡餐参蚕残惭惨灿苍舱仓沧藏操糙槽曹草厕策侧册测层蹭插叉茬茶查碴搽察岔差诧拆柴豺搀掺蝉馋谗缠铲产阐颤昌猖")
    strcat(GT, "场尝常长偿肠厂敞畅唱倡超抄钞朝嘲潮巢吵炒车扯撤掣彻澈郴臣辰尘晨忱沉陈趁衬撑称城橙成呈乘程惩澄诚承逞骋秤吃痴持匙池迟弛驰耻齿侈尺赤翅斥炽充冲虫崇宠抽酬畴踌稠愁筹仇绸瞅丑臭初出橱厨躇锄雏滁除楚")
    strcat(GT, "础储矗搐触处揣川穿椽传船喘串疮窗幢床闯创吹炊捶锤垂春椿醇唇淳纯蠢戳绰疵茨磁雌辞慈瓷词此刺赐次聪葱囱匆从丛凑粗醋簇促蹿篡窜摧崔催脆瘁粹淬翠村存寸磋撮搓措挫错搭达答瘩打大呆歹傣戴带殆代贷袋待逮")
    strcat(GT, "怠耽担丹单郸掸胆旦氮但惮淡诞弹蛋当挡党荡档刀捣蹈倒岛祷导到稻悼道盗德得的蹬灯登等瞪凳邓堤低滴迪敌笛狄涤翟嫡抵底地蒂第帝弟递缔颠掂滇碘点典靛垫电佃甸店惦奠淀殿碉叼雕凋刁掉吊钓调跌爹碟蝶迭谍叠")
    strcat(GT, "丁盯叮钉顶鼎锭定订丢东冬董懂动栋侗恫冻洞兜抖斗陡豆逗痘都督毒犊独读堵睹赌杜镀肚度渡妒端短锻段断缎堆兑队对墩吨蹲敦顿囤钝盾遁掇哆多夺垛躲朵跺舵剁惰堕蛾峨鹅俄额讹娥恶厄扼遏鄂饿恩而儿耳尔饵洱二")
    strcat(GT, "贰发罚筏伐乏阀法珐藩帆番翻樊矾钒繁凡烦反返范贩犯饭泛坊芳方肪房防妨仿访纺放菲非啡飞肥匪诽吠肺废沸费芬酚吩氛分纷坟焚汾粉奋份忿愤粪丰封枫蜂峰锋风疯烽逢冯缝讽奉凤佛否夫敷肤孵扶拂辐幅氟符伏俘服")
    strcat(GT, "浮涪福袱弗甫抚辅俯釜斧脯腑府腐赴副覆赋复傅付阜父腹负富讣附妇缚咐噶嘎该改概钙盖溉干甘杆柑竿肝赶感秆敢赣冈刚钢缸肛纲岗港杠篙皋高膏羔糕搞镐稿告哥歌搁戈鸽胳疙割革葛格蛤阁隔铬个各给根跟耕更庚羹")
    strcat(GT, "埂耿梗工攻功恭龚供躬公宫弓巩汞拱贡共钩勾沟苟狗垢构购够辜菇咕箍估沽孤姑鼓古蛊骨谷股故顾固雇刮瓜剐寡挂褂乖拐怪棺关官冠观管馆罐惯灌贯光广逛瑰规圭硅归龟闺轨鬼诡癸桂柜跪贵刽辊滚棍锅郭国果裹过哈")
    strcat(GT, "骸孩海氦亥害骇酣憨邯韩含涵寒函喊罕翰撼捍旱憾悍焊汗汉夯杭航壕嚎豪毫郝好耗号浩呵喝荷菏核禾和何合盒貉阂河涸赫褐鹤贺嘿黑痕很狠恨哼亨横衡恒轰哄烘虹鸿洪宏弘红喉侯猴吼厚候后呼乎忽瑚壶葫胡蝴狐糊湖")
    strcat(GT, "弧虎唬护互沪户花哗华猾滑画划化话槐徊怀淮坏欢环桓还缓换患唤痪豢焕涣宦幻荒慌黄磺蝗簧皇凰惶煌晃幌恍谎灰挥辉徽恢蛔回毁悔慧卉惠晦贿秽会烩汇讳诲绘荤昏婚魂浑混豁活伙火获或惑霍货祸击圾基机畸稽积箕")
    strcat(GT, "肌饥迹激讥鸡姬绩缉吉极棘辑籍集及急疾汲即嫉级挤几脊己蓟技冀季伎祭剂悸济寄寂计记既忌际妓继纪嘉枷夹佳家加荚颊贾甲钾假稼价架驾嫁歼监坚尖笺间煎兼肩艰奸缄茧检柬碱硷拣捡简俭剪减荐槛鉴践贱见键箭件")
    strcat(GT, "健舰剑饯渐溅涧建僵姜将浆江疆蒋桨奖讲匠酱降蕉椒礁焦胶交郊浇骄娇嚼搅铰矫侥脚狡角饺缴绞剿教酵轿较叫窖揭接皆秸街阶截劫节桔杰捷睫竭洁结解姐戒藉芥界借介疥诫届巾筋斤金今津襟紧锦仅谨进靳晋禁近烬浸")
    strcat(GT, "尽劲荆兢茎睛晶鲸京惊精粳经井警景颈静境敬镜径痉靖竟竞净炯窘揪究纠玖韭久灸九酒厩救旧臼舅咎就疚鞠拘狙疽居驹菊局咀矩举沮聚拒据巨具距踞锯俱句惧炬剧捐鹃娟倦眷卷绢撅攫抉掘倔爵觉决诀绝均菌钧军君峻")
    strcat(GT, "俊竣浚郡骏喀咖卡咯开揩楷凯慨刊堪勘坎砍看康慷糠扛抗亢炕考拷烤靠坷苛柯棵磕颗科壳咳可渴克刻客课肯啃垦恳坑吭空恐孔控抠口扣寇枯哭窟苦酷库裤夸垮挎跨胯块筷侩快宽款匡筐狂框矿眶旷况亏盔岿窥葵奎魁傀")
    strcat(GT, "馈愧溃坤昆捆困括扩廓阔垃拉喇蜡腊辣啦莱来赖蓝婪栏拦篮阑兰澜谰揽览懒缆烂滥琅榔狼廊郎朗浪捞劳牢老佬姥酪烙涝勒乐雷镭蕾磊累儡垒擂肋类泪棱楞冷厘梨犁黎篱狸离漓理李里鲤礼莉荔吏栗丽厉励砾历利傈例俐")
    strcat(GT, "痢立粒沥隶力璃哩俩联莲连镰廉怜涟帘敛脸链恋炼练粮凉梁粱良两辆量晾亮谅撩聊僚疗燎寥辽潦了撂镣廖料列裂烈劣猎琳林磷霖临邻鳞淋凛赁吝拎玲菱零龄铃伶羚凌灵陵岭领另令溜琉榴硫馏留刘瘤流柳六龙聋咙笼窿")
    strcat(GT, "隆垄拢陇楼娄搂篓漏陋芦卢颅庐炉掳卤虏鲁麓碌露路赂鹿潞禄录陆戮驴吕铝侣旅履屡缕虑氯律率滤绿峦挛孪滦卵乱掠略抡轮伦仑沦纶论萝螺罗逻锣箩骡裸落洛骆络妈麻玛码蚂马骂嘛吗埋买麦卖迈脉瞒馒蛮满蔓曼慢漫")
    strcat(GT, "谩芒茫盲氓忙莽猫茅锚毛矛铆卯茂冒帽貌贸么玫枚梅酶霉煤没眉媒镁每美昧寐妹媚门闷们萌蒙檬盟锰猛梦孟眯醚靡糜迷谜弥米秘觅泌蜜密幂棉眠绵冕免勉娩缅面苗描瞄藐秒渺庙妙蔑灭民抿皿敏悯闽明螟鸣铭名命谬摸")
    strcat(GT, "摹蘑模膜磨摩魔抹末莫墨默沫漠寞陌谋牟某拇牡亩姆母墓暮幕募慕木目睦牧穆拿哪呐钠那娜纳氖乃奶耐奈南男难囊挠脑恼闹淖呢馁内嫩能妮霓倪泥尼拟你匿腻逆溺蔫拈年碾撵捻念娘酿鸟尿捏聂孽啮镊镍涅您柠狞凝宁")
    strcat(GT, "拧泞牛扭钮纽脓浓农弄奴努怒女暖虐疟挪懦糯诺哦欧鸥殴藕呕偶沤啪趴爬帕怕琶拍排牌徘湃派攀潘盘磐盼畔判叛乓庞旁耪胖抛咆刨炮袍跑泡呸胚培裴赔陪配佩沛喷盆砰抨烹澎彭蓬棚硼篷膨朋鹏捧碰坯砒霹批披劈琵毗")
    strcat(GT, "啤脾疲皮匹痞僻屁譬篇偏片骗飘漂瓢票撇瞥拼频贫品聘乒坪苹萍平凭瓶评屏坡泼颇婆破魄迫粕剖扑铺仆莆葡菩蒲埔朴圃普浦谱曝瀑期欺栖戚妻七凄漆柒沏其棋奇歧畦崎脐齐旗祈祁骑起岂乞企启契砌器气迄弃汽泣讫掐")
    strcat(GT, "恰洽牵扦钎铅千迁签仟谦乾黔钱钳前潜遣浅谴堑嵌欠歉枪呛腔羌墙蔷强抢橇锹敲悄桥瞧乔侨巧鞘撬翘峭俏窍切茄且怯窃钦侵亲秦琴勤芹擒禽寝沁青轻氢倾卿清擎晴氰情顷请庆琼穷秋丘邱球求囚酋泅趋区蛆曲躯屈驱渠")
    strcat(GT, "取娶龋趣去圈颧权醛泉全痊拳犬券劝缺炔瘸却鹊榷确雀裙群然燃冉染瓤壤攘嚷让饶扰绕惹热壬仁人忍韧任认刃妊纫扔仍日戎茸蓉荣融熔溶容绒冗揉柔肉茹蠕儒孺如辱乳汝入褥软阮蕊瑞锐闰润若弱撒洒萨腮鳃塞赛三叁")
    strcat(GT, "伞散桑嗓丧搔骚扫嫂瑟色涩森僧莎砂杀刹沙纱傻啥煞筛晒珊苫杉山删煽衫闪陕擅赡膳善汕扇缮墒伤商赏晌上尚裳梢捎稍烧芍勺韶少哨邵绍奢赊蛇舌舍赦摄射慑涉社设砷申呻伸身深娠绅神沈审婶甚肾慎渗声生甥牲升绳")
    strcat(GT, "省盛剩胜圣师失狮施湿诗尸虱十石拾时什食蚀实识史矢使屎驶始式示士世柿事拭誓逝势是嗜噬适仕侍释饰氏市恃室视试收手首守寿授售受瘦兽蔬枢梳殊抒输叔舒淑疏书赎孰熟薯暑曙署蜀黍鼠属术述树束戍竖墅庶数漱")
    strcat(GT, "恕刷耍摔衰甩帅栓拴霜双爽谁水睡税吮瞬顺舜说硕朔烁斯撕嘶思私司丝死肆寺嗣四伺似饲巳松耸怂颂送宋讼诵搜艘擞嗽苏酥俗素速粟僳塑溯宿诉肃酸蒜算虽隋随绥髓碎岁穗遂隧祟孙损笋蓑梭唆缩琐索锁所塌他它她塔")
    strcat(GT, "獭挞蹋踏胎苔抬台泰酞太态汰坍摊贪瘫滩坛檀痰潭谭谈坦毯袒碳探叹炭汤塘搪堂棠膛唐糖倘躺淌趟烫掏涛滔绦萄桃逃淘陶讨套特藤腾疼誊梯剔踢锑提题蹄啼体替嚏惕涕剃屉天添填田甜恬舔腆挑条迢眺跳贴铁帖厅听烃")
    strcat(GT, "汀廷停亭庭挺艇通桐酮瞳同铜彤童桶捅筒统痛偷投头透凸秃突图徒途涂屠土吐兔湍团推颓腿蜕褪退吞屯臀拖托脱鸵陀驮驼椭妥拓唾挖哇蛙洼娃瓦袜歪外豌弯湾玩顽丸烷完碗挽晚皖惋宛婉万腕汪王亡枉网往旺望忘妄威")
    strcat(GT, "巍微危韦违桅围唯惟为潍维苇萎委伟伪尾纬未蔚味畏胃喂魏位渭谓尉慰卫瘟温蚊文闻纹吻稳紊问嗡翁瓮挝蜗涡窝我斡卧握沃巫呜钨乌污诬屋无芜梧吾吴毋武五捂午舞伍侮坞戊雾晤物勿务悟误昔熙析西硒矽晰嘻吸锡牺")
    strcat(GT, "稀息希悉膝夕惜熄烯溪汐犀檄袭席习媳喜铣洗系隙戏细瞎虾匣霞辖暇峡侠狭下厦夏吓掀锨先仙鲜纤咸贤衔舷闲涎弦嫌显险现献县腺馅羡宪陷限线相厢镶香箱襄湘乡翔祥详想响享项巷橡像向象萧硝霄削哮嚣销消宵淆晓")
    strcat(GT, "小孝校肖啸笑效楔些歇蝎鞋协挟携邪斜胁谐写械卸蟹懈泄泻谢屑薪芯锌欣辛新忻心信衅星腥猩惺兴刑型形邢行醒幸杏性姓兄凶胸匈汹雄熊休修羞朽嗅锈秀袖绣墟戌需虚嘘须徐许蓄酗叙旭序畜恤絮婿绪续轩喧宣悬旋玄")
    strcat(GT, "选癣眩绚靴薛学穴雪血勋熏循旬询寻驯巡殉汛训讯逊迅压押鸦鸭呀丫芽牙蚜崖衙涯雅哑亚讶焉咽阉烟淹盐严研蜒岩延言颜阎炎沿奄掩眼衍演艳堰燕厌砚雁唁彦焰宴谚验殃央鸯秧杨扬佯疡羊洋阳氧仰痒养样漾邀腰妖瑶")
    strcat(GT, "摇尧遥窑谣姚咬舀药要耀椰噎耶爷野冶也页掖业叶曳腋夜液一壹医揖铱依伊衣颐夷遗移仪胰疑沂宜姨彝椅蚁倚已乙矣以艺抑易邑屹亿役臆逸肄疫亦裔意毅忆义益溢诣议谊译异翼翌绎茵荫因殷音阴姻吟银淫寅饮尹引隐")
    strcat(GT, "印英樱婴鹰应缨莹萤营荧蝇迎赢盈影颖硬映哟拥佣臃痈庸雍踊蛹咏泳涌永恿勇用幽优悠忧尤由邮铀犹油游酉有友右佑釉诱又幼迂淤于盂榆虞愚舆余俞逾鱼愉渝渔隅予娱雨与屿禹宇语羽玉域芋郁吁遇喻峪御愈欲狱育誉")
    strcat(GT, "浴寓裕预豫驭鸳渊冤元垣袁原援辕园员圆猿源缘远苑愿怨院曰约越跃钥岳粤月悦阅耘云郧匀陨允运蕴酝晕韵孕匝砸杂栽哉灾宰载再在咱攒暂赞赃脏葬遭糟凿藻枣早澡蚤躁噪造皂灶燥责择则泽贼怎增憎曾赠扎喳渣札轧")
    strcat(GT, "铡闸眨栅榨咋乍炸诈摘斋宅窄债寨瞻毡詹粘沾盏斩辗崭展蘸栈占战站湛绽樟章彰漳张掌涨杖丈帐账仗胀瘴障招昭找沼赵照罩兆肇召遮折哲蛰辙者锗蔗这浙珍斟真甄砧臻贞针侦枕疹诊震振镇阵蒸挣睁征狰争怔整拯正政")
    strcat(GT, "帧症郑证芝枝支吱蜘知肢脂汁之织职直植殖执值侄址指止趾只旨纸志挚掷至致置帜峙制智秩稚质炙痔滞治窒中盅忠钟衷终种肿重仲众舟周州洲诌粥轴肘帚咒皱宙昼骤珠株蛛朱猪诸诛逐竹烛煮拄瞩嘱主著柱助蛀贮铸筑")
    strcat(GT, "住注祝驻抓爪拽专砖转撰赚篆桩庄装妆撞壮状椎锥追赘坠缀谆准捉拙卓桌琢茁酌啄着灼浊兹咨资姿滋淄孜紫仔籽滓子自渍字鬃棕踪宗综总纵邹走奏揍租足卒族祖诅阻组钻纂嘴醉最罪尊遵昨左佐柞做作坐座？？？？？")
    strcat(GT, "亍丌兀丐廿卅丕亘丞鬲孬噩丨禺丿匕乇夭爻卮氐囟胤馗毓睾鼗丶亟鼐乜乩亓芈孛啬嘏仄厍厝厣厥厮靥赝匚叵匦匮匾赜卦卣刂刈刎刭刳刿剀剌剞剡剜蒯剽劂劁劐劓冂罔亻仃仉仂仨仡仫仞伛仳伢佤仵伥伧伉伫佞佧攸佚佝")
    strcat(GT, "佟佗伲伽佶佴侑侉侃侏佾佻侪佼侬侔俦俨俪俅俚俣俜俑俟俸倩偌俳倬倏倮倭俾倜倌倥倨偾偃偕偈偎偬偻傥傧傩傺僖儆僭僬僦僮儇儋仝氽佘佥俎龠汆籴兮巽黉馘冁夔勹匍訇匐凫夙兕亠兖亳衮袤亵脔裒禀嬴蠃羸冫冱冽冼")
    strcat(GT, "凇冖冢冥讠讦讧讪讴讵讷诂诃诋诏诎诒诓诔诖诘诙诜诟诠诤诨诩诮诰诳诶诹诼诿谀谂谄谇谌谏谑谒谔谕谖谙谛谘谝谟谠谡谥谧谪谫谮谯谲谳谵谶卩卺阝阢阡阱阪阽阼陂陉陔陟陧陬陲陴隈隍隗隰邗邛邝邙邬邡邴邳邶邺")
    strcat(GT, "邸邰郏郅邾郐郄郇郓郦郢郜郗郛郫郯郾鄄鄢鄞鄣鄱鄯鄹酃酆刍奂劢劬劭劾哿勐勖勰叟燮矍廴凵凼鬯厶弁畚巯坌垩垡塾墼壅壑圩圬圪圳圹圮圯坜圻坂坩垅坫垆坼坻坨坭坶坳垭垤垌垲埏垧垴垓垠埕埘埚埙埒垸埴埯埸埤埝")
    strcat(GT, "堋堍埽埭堀堞堙塄堠塥塬墁墉墚墀馨鼙懿艹艽艿芏芊芨芄芎芑芗芙芫芸芾芰苈苊苣芘芷芮苋苌苁芩芴芡芪芟苄苎芤苡茉苷苤茏茇苜苴苒苘茌苻苓茑茚茆茔茕苠苕茜荑荛荜茈莒茼茴茱莛荞茯荏荇荃荟荀茗荠茭茺茳荦荥")
    strcat(GT, "荨茛荩荬荪荭荮莰荸莳莴莠莪莓莜莅荼莶莩荽莸荻莘莞莨莺莼菁萁菥菘堇萘萋菝菽菖萜萸萑萆菔菟萏萃菸菹菪菅菀萦菰菡葜葑葚葙葳蒇蒈葺蒉葸萼葆葩葶蒌蒎萱葭蓁蓍蓐蓦蒽蓓蓊蒿蒺蓠蒡蒹蒴蒗蓥蓣蔌甍蔸蓰蔹蔟蔺")
    strcat(GT, "蕖蔻蓿蓼蕙蕈蕨蕤蕞蕺瞢蕃蕲蕻薤薨薇薏蕹薮薜薅薹薷薰藓藁藜藿蘧蘅蘩蘖蘼廾弈夼奁耷奕奚奘匏尢尥尬尴扌扪抟抻拊拚拗拮挢拶挹捋捃掭揶捱捺掎掴捭掬掊捩掮掼揲揸揠揿揄揞揎摒揆掾摅摁搋搛搠搌搦搡摞撄摭撖")
    strcat(GT, "摺撷撸撙撺擀擐擗擤擢攉攥攮弋忒甙弑卟叱叽叩叨叻吒吖吆呋呒呓呔呖呃吡呗呙吣吲咂咔呷呱呤咚咛咄呶呦咝哐咭哂咴哒咧咦哓哔呲咣哕咻咿哌哙哚哜咩咪咤哝哏哞唛哧唠哽唔哳唢唣唏唑唧唪啧喏喵啉啭啁啕唿啐唼")
    strcat(GT, "唷啖啵啶啷唳唰啜喋嗒喃喱喹喈喁喟啾嗖喑啻嗟喽喾喔喙嗪嗷嗉嘟嗑嗫嗬嗔嗦嗝嗄嗯嗥嗲嗳嗌嗍嗨嗵嗤辔嘞嘈嘌嘁嘤嘣嗾嘀嘧嘭噘嘹噗嘬噍噢噙噜噌噔嚆噤噱噫噻噼嚅嚓嚯囔囗囝囡囵囫囹囿圄圊圉圜帏帙帔帑帱帻帼")
    strcat(GT, "帷幄幔幛幞幡岌屺岍岐岖岈岘岙岑岚岜岵岢岽岬岫岱岣峁岷峄峒峤峋峥崂崃崧崦崮崤崞崆崛嵘崾崴崽嵬嵛嵯嵝嵫嵋嵊嵩嵴嶂嶙嶝豳嶷巅彳彷徂徇徉後徕徙徜徨徭徵徼衢彡犭犰犴犷犸狃狁狎狍狒狨狯狩狲狴狷猁狳猃狺")
    strcat(GT, "狻猗猓猡猊猞猝猕猢猹猥猬猸猱獐獍獗獠獬獯獾舛夥飧夤夂饣饧饨饩饪饫饬饴饷饽馀馄馇馊馍馐馑馓馔馕庀庑庋庖庥庠庹庵庾庳赓廒廑廛廨廪膺忄忉忖忏怃忮怄忡忤忾怅怆忪忭忸怙怵怦怛怏怍怩怫怊怿怡恸恹恻恺恂")
    strcat(GT, "恪恽悖悚悭悝悃悒悌悛惬悻悱惝惘惆惚悴愠愦愕愣惴愀愎愫慊慵憬憔憧憷懔懵忝隳闩闫闱闳闵闶闼闾阃阄阆阈阊阋阌阍阏阒阕阖阗阙阚丬爿戕氵汔汜汊沣沅沐沔沌汨汩汴汶沆沩泐泔沭泷泸泱泗沲泠泖泺泫泮沱泓泯泾")
    strcat(GT, "洹洧洌浃浈洇洄洙洎洫浍洮洵洚浏浒浔洳涑浯涞涠浞涓涔浜浠浼浣渚淇淅淞渎涿淠渑淦淝淙渖涫渌涮渫湮湎湫溲湟溆湓湔渲渥湄滟溱溘滠漭滢溥溧溽溻溷滗溴滏溏滂溟潢潆潇漤漕滹漯漶潋潴漪漉漩澉澍澌潸潲潼潺濑")
    strcat(GT, "濉澧澹澶濂濡濮濞濠濯瀚瀣瀛瀹瀵灏灞宀宄宕宓宥宸甯骞搴寤寮褰寰蹇謇辶迓迕迥迮迤迩迦迳迨逅逄逋逦逑逍逖逡逵逶逭逯遄遑遒遐遨遘遢遛暹遴遽邂邈邃邋彐彗彖彘尻咫屐屙孱屣屦羼弪弩弭艴弼鬻屮妁妃妍妩妪妣")
    strcat(GT, "妗姊妫妞妤姒妲妯姗妾娅娆姝娈姣姘姹娌娉娲娴娑娣娓婀婧婊婕娼婢婵胬媪媛婷婺媾嫫媲嫒嫔媸嫠嫣嫱嫖嫦嫘嫜嬉嬗嬖嬲嬷孀尕尜孚孥孳孑孓孢驵驷驸驺驿驽骀骁骅骈骊骐骒骓骖骘骛骜骝骟骠骢骣骥骧纟纡纣纥纨纩")
    strcat(GT, "纭纰纾绀绁绂绉绋绌绐绔绗绛绠绡绨绫绮绯绱绲缍绶绺绻绾缁缂缃缇缈缋缌缏缑缒缗缙缜缛缟缡缢缣缤缥缦缧缪缫缬缭缯缰缱缲缳缵幺畿巛甾邕玎玑玮玢玟珏珂珑玷玳珀珉珈珥珙顼琊珩珧珞玺珲琏琪瑛琦琥琨琰琮琬")
    strcat(GT, "琛琚瑁瑜瑗瑕瑙瑷瑭瑾璜璎璀璁璇璋璞璨璩璐璧瓒璺韪韫韬杌杓杞杈杩枥枇杪杳枘枧杵枨枞枭枋杷杼柰栉柘栊柩枰栌柙枵柚枳柝栀柃枸柢栎柁柽栲栳桠桡桎桢桄桤梃栝桕桦桁桧桀栾桊桉栩梵梏桴桷梓桫棂楮棼椟椠棹")
    strcat(GT, "椤棰椋椁楗棣椐楱椹楠楂楝榄楫榀榘楸椴槌榇榈槎榉楦楣楹榛榧榻榫榭槔榱槁槊槟榕槠榍槿樯槭樗樘橥槲橄樾檠橐橛樵檎橹樽樨橘橼檑檐檩檗檫猷獒殁殂殇殄殒殓殍殚殛殡殪轫轭轱轲轳轵轶轸轷轹轺轼轾辁辂辄辇辋")
    strcat(GT, "辍辎辏辘辚軎戋戗戛戟戢戡戥戤戬臧瓯瓴瓿甏甑甓攴旮旯旰昊昙杲昃昕昀炅曷昝昴昱昶昵耆晟晔晁晏晖晡晗晷暄暌暧暝暾曛曜曦曩贲贳贶贻贽赀赅赆赈赉赇赍赕赙觇觊觋觌觎觏觐觑牮犟牝牦牯牾牿犄犋犍犏犒挈挲掰")
    strcat(GT, "搿擘耄毪毳毽毵毹氅氇氆氍氕氘氙氚氡氩氤氪氲攵敕敫牍牒牖爰虢刖肟肜肓肼朊肽肱肫肭肴肷胧胨胩胪胛胂胄胙胍胗朐胝胫胱胴胭脍脎胲胼朕脒豚脶脞脬脘脲腈腌腓腴腙腚腱腠腩腼腽腭腧塍媵膈膂膑滕膣膪臌朦臊膻")
    strcat(GT, "臁膦欤欷欹歃歆歙飑飒飓飕飙飚殳彀毂觳斐齑斓於旆旄旃旌旎旒旖炀炜炖炝炻烀炷炫炱烨烊焐焓焖焯焱煳煜煨煅煲煊煸煺熘熳熵熨熠燠燔燧燹爝爨灬焘煦熹戾戽扃扈扉礻祀祆祉祛祜祓祚祢祗祠祯祧祺禅禊禚禧禳忑忐")
    strcat(GT, "怼恝恚恧恁恙恣悫愆愍慝憩憝懋懑戆肀聿沓泶淼矶矸砀砉砗砘砑斫砭砜砝砹砺砻砟砼砥砬砣砩硎硭硖硗砦硐硇硌硪碛碓碚碇碜碡碣碲碹碥磔磙磉磬磲礅磴礓礤礞礴龛黹黻黼盱眄眍盹眇眈眚眢眙眭眦眵眸睐睑睇睃睚睨")
    strcat(GT, "睢睥睿瞍睽瞀瞌瞑瞟瞠瞰瞵瞽町畀畎畋畈畛畲畹疃罘罡罟詈罨罴罱罹羁罾盍盥蠲钅钆钇钋钊钌钍钏钐钔钗钕钚钛钜钣钤钫钪钭钬钯钰钲钴钶钷钸钹钺钼钽钿铄铈铉铊铋铌铍铎铐铑铒铕铖铗铙铘铛铞铟铠铢铤铥铧铨铪")
    strcat(GT, "铩铫铮铯铳铴铵铷铹铼铽铿锃锂锆锇锉锊锍锎锏锒锓锔锕锖锘锛锝锞锟锢锪锫锩锬锱锲锴锶锷锸锼锾锿镂锵镄镅镆镉镌镎镏镒镓镔镖镗镘镙镛镞镟镝镡镢镤镥镦镧镨镩镪镫镬镯镱镲镳锺矧矬雉秕秭秣秫稆嵇稃稂稞稔")
    strcat(GT, "稹稷穑黏馥穰皈皎皓皙皤瓞瓠甬鸠鸢鸨鸩鸪鸫鸬鸲鸱鸶鸸鸷鸹鸺鸾鹁鹂鹄鹆鹇鹈鹉鹋鹌鹎鹑鹕鹗鹚鹛鹜鹞鹣鹦鹧鹨鹩鹪鹫鹬鹱鹭鹳疒疔疖疠疝疬疣疳疴疸痄疱疰痃痂痖痍痣痨痦痤痫痧瘃痱痼痿瘐瘀瘅瘌瘗瘊瘥瘘瘕瘙")
    strcat(GT, "瘛瘼瘢瘠癀瘭瘰瘿瘵癃瘾瘳癍癞癔癜癖癫癯翊竦穸穹窀窆窈窕窦窠窬窨窭窳衤衩衲衽衿袂袢裆袷袼裉裢裎裣裥裱褚裼裨裾裰褡褙褓褛褊褴褫褶襁襦襻疋胥皲皴矜耒耔耖耜耠耢耥耦耧耩耨耱耋耵聃聆聍聒聩聱覃顸颀颃")
    strcat(GT, "颉颌颍颏颔颚颛颞颟颡颢颥颦虍虔虬虮虿虺虼虻蚨蚍蚋蚬蚝蚧蚣蚪蚓蚩蚶蛄蚵蛎蚰蚺蚱蚯蛉蛏蚴蛩蛱蛲蛭蛳蛐蜓蛞蛴蛟蛘蛑蜃蜇蛸蜈蜊蜍蜉蜣蜻蜞蜥蜮蜚蜾蝈蜴蜱蜩蜷蜿螂蜢蝽蝾蝻蝠蝰蝌蝮螋蝓蝣蝼蝤蝙蝥螓螯螨蟒")
    strcat(GT, "蟆螈螅螭螗螃螫蟥螬螵螳蟋蟓螽蟑蟀蟊蟛蟪蟠蟮蠖蠓蟾蠊蠛蠡蠹蠼缶罂罄罅舐竺竽笈笃笄笕笊笫笏筇笸笪笙笮笱笠笥笤笳笾笞筘筚筅筵筌筝筠筮筻筢筲筱箐箦箧箸箬箝箨箅箪箜箢箫箴篑篁篌篝篚篥篦篪簌篾篼簏簖簋")
    strcat(GT, "簟簪簦簸籁籀臾舁舂舄臬衄舡舢舣舭舯舨舫舸舻舳舴舾艄艉艋艏艚艟艨衾袅袈裘裟襞羝羟羧羯羰羲籼敉粑粝粜粞粢粲粼粽糁糇糌糍糈糅糗糨艮暨羿翎翕翥翡翦翩翮翳糸絷綦綮繇纛麸麴赳趄趔趑趱赧赭豇豉酊酐酎酏酤")
    strcat(GT, "酢酡酰酩酯酽酾酲酴酹醌醅醐醍醑醢醣醪醭醮醯醵醴醺豕鹾趸跫踅蹙蹩趵趿趼趺跄跖跗跚跞跎跏跛跆跬跷跸跣跹跻跤踉跽踔踝踟踬踮踣踯踺蹀踹踵踽踱蹉蹁蹂蹑蹒蹊蹰蹶蹼蹯蹴躅躏躔躐躜躞豸貂貊貅貘貔斛觖觞觚觜")
    strcat(GT, "觥觫觯訾謦靓雩雳雯霆霁霈霏霎霪霭霰霾龀龃龅龆龇龈龉龊龌黾鼋鼍隹隼隽雎雒瞿雠銎銮鋈錾鍪鏊鎏鐾鑫鱿鲂鲅鲆鲇鲈稣鲋鲎鲐鲑鲒鲔鲕鲚鲛鲞鲟鲠鲡鲢鲣鲥鲦鲧鲨鲩鲫鲭鲮鲰鲱鲲鲳鲴鲵鲶鲷鲺鲻鲼鲽鳄鳅鳆鳇鳊鳋")
    strcat(GT, "鳌鳍鳎鳏鳐鳓鳔鳕鳗鳘鳙鳜鳝鳟鳢靼鞅鞑鞒鞔鞯鞫鞣鞲鞴骱骰骷鹘骶骺骼髁髀髅髂髋髌髑魅魃魇魉魈魍魑飨餍餮饕饔髟髡髦髯髫髻髭髹鬈鬏鬓鬟鬣麽麾縻麂麇麈麋麒鏖麝麟黛黜黝黠黟黢黩黧黥黪黯鼢鼬鼯鼹鼷鼽鼾齄")
    # 表拼完**自证一次**：6768 项 × 3 字节 = 20304（这是判据，不是装饰 —— 少一行 `strcat`
    # Self-check once after building: 6768 entries x 3 bytes = 20304 (a criterion, not decoration --
    # 不会报错，只会让后面所有下标整体错位）。
    # a missing `strcat` line raises nothing and just shifts every index).
    print_str("[gtab] bytes=")
    print_int(strlen(GT))
    print_str("\n")


# ── 小工具 ────────────────────────────────────────────
# ── small helpers ─────────────────────────────────────

# 从 `RBUF` 的 [start, start+len) 抄一段进 dst（补 0）。边界自己钳到 `LIM`。
# Copy [start, start+len) out of `RBUF` into dst (NUL-terminated), clamping to `LIM` by hand.
# ⚠ `str_substr` 只按 `strlen(src)` 钳，所以"不许越过正文右边界"这件事得自己做。
# ⚠ `str_substr` clamps by `strlen(src)` only, so the body's right edge has to be enforced here.
def copy_slice(start, length, dst, cap):
    end = start + length
    n = 0
    if end > LIM:
        end = LIM
    n = end - start
    if n < 0:
        n = 0
    if n > cap - 1:
        n = cap - 1
    str_substr(dst, RBUF, start, n)
    return n


# GBK 的 [start, start+len) → UTF-8 写进 dst（补 0）。ASCII 直通，落在 GB2312 那 72 个区里的查表，
# GBK [start, start+len) to UTF-8 in dst (NUL-terminated): ASCII passes through, the 72 GB2312
# 其余（GBK 扩展区）回 `?`。返回写出的字节数。
# zones are looked up, and anything else (the GBK extensions) becomes `?`. Returns bytes written.
# ⚠ 不用 `encoding_gbk_decode`：它是近似式，见文件头。
# ⚠ Not `encoding_gbk_decode`: that one is an approximation, see the file header.
def gbk_to_utf8(start, length, dst, cap):
    p = start
    end = start + length
    o = 0
    b0 = 0
    b1 = 0
    idx = 0
    if end > LIM:
        end = LIM
    while p < end and o < cap - 1:
        b0 = str_charat(RBUF, p) & 255
        if b0 <= 0x7F:
            memset(dst + o, b0, 1)
            o = o + 1
            p = p + 1
        elif p + 1 >= LIM:
            memset(dst + o, 63, 1)          # 半角 `?`
            # a half-width `?`
            o = o + 1
            p = p + 1
        else:
            b1 = str_charat(RBUF, p + 1) & 255
            if b0 < 0x81:
                memset(dst + o, 63, 1)
                o = o + 1
                p = p + 1
            else:
                if b0 >= 0xB0 and b0 <= 0xF7 and b1 >= 0xA1 and b1 <= 0xFE:
                    idx = (b0 - 0xB0) * 94 + (b1 - 0xA1)
                    str_substr(dst + o, GT, idx * 3, 3)
                    o = o + 3
                else:
                    memset(dst + o, 63, 1)  # GBK 扩展区，表里没有
                    # a GBK extension, not in the table
                    o = o + 1
                p = p + 2
    memset(dst + o, 0, 1)
    return o


# "3.27" / "-1.50" / "+0.00" → ×100 的整数（327 / -150 / 0），认不出来回 0。
# "3.27" / "-1.50" / "+0.00" to an integer x100 (327 / -150 / 0); 0 when it cannot be read.
# ⚠ 只收**两位小数**：行情接口给的正好是两位（实测 "3.27"、"1.94"）。
# ⚠ **Two decimals only**: the feed hands back exactly two (measured "3.27", "1.94").
def parse_signed_100(s):
    i = 0
    neg = 0
    v = 0
    c = str_charat(s, 0)
    if c == 45:                 # '-' / minus
        neg = 1
        i = 1
    elif c == 43:               # '+' / plus
        i = 1
    c = str_charat(s, i)
    while c >= 48 and c <= 57:
        v = v * 10 + (c - 48)
        i = i + 1
        c = str_charat(s, i)
    v = v * 100
    if c == 46:                 # '.' / the decimal point
        i = i + 1
        c = str_charat(s, i)
        if c >= 48 and c <= 57:
            v = v + (c - 48) * 10
            i = i + 1
            c = str_charat(s, i)
        if c >= 48 and c <= 57:
            v = v + (c - 48)
    if neg == 1:
        return 0 - v
    return v


# ⚠ 这里原先有两个手写的协议辅助：`find_body`（找 `\r\n\r\n`）与 `http_code`（解析状态码）——
#   库 `Lib/shared/src/http.c` 的 `http_get_to` 已经把剥头与状态码都做了，只往回装**正文**。
# The two hand-written protocol helpers used to live here; `http_get_to` covers both.
# 已经攒了几条记录（`v_` 且在本行行首），给读循环加一道"够了就收"的保险。
# How many records have arrived so far (a `v_` at the start of a line) -- the read loop's early stop.
def count_records(total):
    i = 0
    c = 0
    while i < total:
        if str_charat(RBUF, i) == 118:              # 'v'
            if i == 0:
                c = c + 1
            elif (str_charat(RBUF, i - 1) & 255) == 10:
                c = c + 1
        i = i + 1
    return c


# ── 解析（在线的行情行）───────────────────────────────
# ── parsing (the live quote line) ─────────────────────
# 记录形状：`v_sh600000="0~名称~代码~现价~昨收~今开~…~31 涨跌额~32 涨跌幅~…";`
# Record shape: `v_sh600000="0~name~code~price~prevClose~open~...~31 change~32 change%~...";`
# ⚠ 字段边界只有 `~` 和收尾的 `"` —— **不要拿空格当分界**（实测 `sz000858` 的名称是 `五 粮 液`）。
# ⚠ The only field boundaries are `~` and the closing `"` -- **never treat a space as one**
#   (measured: `sz000858` comes back with a space between each pair of hanzi).
# 返回解析出的只数（顶层赋给 `N_STK`）。
# Returns how many were parsed (the top level assigns it to `N_STK`).
def parse_quote(head, blen):
    sn = 0
    i = head
    end = head + blen
    fi = 0
    fs = 0
    ok = 0
    c = 0
    while i < end and sn < MAXSTK:
        if str_charat(RBUF, i) != 118:
            i = i + 1
        else:
            while i < end:
                if str_charat(RBUF, i) == 61 and i + 1 < end and str_charat(RBUF, i + 1) == 34:
                    break
                i = i + 1
            if i >= end:
                break
            i = i + 2                        # 跳过 `="`，站到字段 0 上
            # step over `="`
            fi = 0
            fs = i
            ok = 0
            while i < end:
                c = str_charat(RBUF, i) & 255
                if c == 126 or c == 34:      # '~' or '"'
                    if fi == 1:
                        gbk_to_utf8(fs, i - fs, S_NAME + sn * 32, 32)
                    elif fi == 2:
                        copy_slice(fs, i - fs, S_CODE + sn * 12, 12)
                    elif fi == 3:
                        copy_slice(fs, i - fs, S_PRICE + sn * 16, 16)
                    elif fi == 32:
                        copy_slice(fs, i - fs, S_PCT + sn * 16, 16)
                    if c == 34:
                        break
                    fi = fi + 1
                    i = i + 1
                    fs = i
                else:
                    i = i + 1
            if fi >= 32:                     # 走到字段 32 才收
            # only if field 32 was reached
                ok = 1
            if ok != 0:
                S_PCT100[sn] = parse_signed_100(S_PCT + sn * 16)
                sn = sn + 1
            i = i + 1
    return sn


# ── 解析（离线的 stocks.txt）──────────────────────────
# ── parsing (the offline stocks.txt) ──────────────────
# `代码|名称|现价|涨跌幅`，`#` 开头是注释。名称本来就是 UTF-8，原样抄。
# `code|name|price|change%`; a leading `#` is a comment. The name is already UTF-8, copied as is.
# ⚠ 凑不满 4 段的整行丢掉（宁可少一张卡，不要一张半空的卡）。
# ⚠ A line with fewer than four fields is dropped (better one card short than half a blank one).
def parse_local_line(start, end, n0):
    p = start
    fs = start
    fi = 0
    hit = 0
    sn = n0
    # ⚠ **形参不能赋值**：本前端对"函数体里给形参赋值"会生成一个越界的寄存器引用
    # ⚠ **A parameter cannot be assigned to**: this front end emits an out-of-range register
    #   而整个文件**编不过**（实测 4 行最小复现：`def g(a, b): b = b + 1` ⇒
    #   reference for it and the whole file **fails to compile** (measured with a 4-line repro:
    #   `错误: 寄存器越界: [R1216]`），所以这里先把形参抄进局部量再改。
    #   `def g(a, b): b = b + 1` => `error: register out of range: [R1216]`), hence the local copy.
    while p <= end:
        hit = 0
        if p == end:
            hit = 1
        elif str_charat(RBUF, p) == 124:     # '|'
            hit = 1
        if hit == 1:
            if fi == 0:
                copy_slice(fs, p - fs, S_CODE + sn * 12, 12)
            elif fi == 1:
                copy_slice(fs, p - fs, S_NAME + sn * 32, 32)
            elif fi == 2:
                copy_slice(fs, p - fs, S_PRICE + sn * 16, 16)
            elif fi == 3:
                copy_slice(fs, p - fs, S_PCT + sn * 16, 16)
            fi = fi + 1
            fs = p + 1
        p = p + 1
    if fi >= 4 and sn < MAXSTK:
        S_PCT100[sn] = parse_signed_100(S_PCT + sn * 16)
        sn = sn + 1
    return sn


# 把整份本地文件切行（`\n` 或 `\r\n`），`#` 开头的行跳过。返回解析出的只数。
# Split the whole local file into lines (`\n` or `\r\n`) and skip lines starting with `#`.
def parse_local(blen):
    p = 0
    ls = 0
    le = 0
    sn = 0
    while p <= blen and sn < MAXSTK:
        if p == blen or (str_charat(RBUF, p) & 255) == 10:
            le = p
            if le > ls and (str_charat(RBUF, le - 1) & 255) == 13:
                le = le - 1
            if le > ls and str_charat(RBUF, ls) != 35:      # '#'
                sn = parse_local_line(ls, le, sn)
            ls = p + 1
        p = p + 1
    return sn


# ── 版面与绘制 ────────────────────────────────────────
# ── layout and drawing ────────────────────────────────

def scr_w():
    w = ui_scr_get_w()
    if w <= 0:
        w = 360
    return w


def scr_h():
    h = ui_scr_get_h()
    if h <= 0:
        h = 620
    return h


def draw():
    # 版面**整段算在 draw 里**：函数改不了模块级标量（实测），而这些量只有本函数用。
    # The whole layout is computed **inside `draw`**: a function cannot assign module-level scalars
    # (measured), and nothing outside this function needs them.
    cx = SW // 2
    pad = SW // 18
    top = 0
    avail = 0
    cardh = 0
    i = 0
    y = 0
    h = 0
    by = 0
    ix = 0
    iw = 0
    mid = 0
    amt = 0
    bw = 0
    col = 0
    c0 = 0
    if pad < 6:
        pad = 6
    top = pad + 62                    # 标题两行之下的位置
    # where the two heading lines end
    avail = SH - top - 34             # 底下留一行提示
    # leave one line for the footer
    if N_STK > 0:
        cardh = avail // N_STK
    else:
        cardh = avail
    if cardh > 96:                    # 卡少了别把它们拉得老长
    # do not stretch a few cards
        cardh = 96
    if cardh < 52:
        cardh = 52
    # 卡片块在可用高度里**居中**：只有三四只时，顶着标题下面会留一大片空。
    # **Centre** the block in the available height: with three or four cards, hugging the heading
    # leaves a big gap that reads as "something is missing below".
    top = top + (avail - N_STK * cardh) // 2

    ui_2d_clear(C_BG)
    ui_2d_text_styled(cx, pad, "股票行情" if LANG == 0 else "Stock Quotes",
                      C_TITLE, 17, VML_ANCHOR_CENTER, VML_FONT_BOLD)
    if OFFLINE != 0:
        ui_2d_text(cx, pad + 26, "离线：读本地 stocks.txt" if LANG == 0
                   else "offline: reading local stocks.txt", C_WARN, 11, VML_ANCHOR_CENTER)
    else:
        ui_2d_text(cx, pad + 26, "数据源 qt.gtimg.cn" if LANG == 0
                   else "source qt.gtimg.cn", C_DIM, 11, VML_ANCHOR_CENTER)

    if N_STK <= 0:
        ui_2d_text(cx, SH // 2 - 8, "没有数据" if LANG == 0 else "no data", C_DIM, 14,
                   VML_ANCHOR_CENTER)
    else:
        i = 0
        while i < N_STK:
            y = top + i * cardh
            h = cardh - 6
            ui_2d_rect(pad, y, SW - pad * 2, h, C_CARD, 1, 0, 10)

            # 代码（左上，暗）
            # The code (top left, dim)
            ui_2d_text(pad + 12, y + 9, S_CODE + i * 12, C_DIM, 11, VML_ANCHOR_LEFT)

            # 涨跌幅（右上，带符号；涨红跌绿）
            # The change percent (top right, signed; red up, green down)
            col = C_DIM
            if S_PCT100[i] > 0:
                col = C_UP
            elif S_PCT100[i] < 0:
                col = C_DOWN
            memset(S_PB, 0, 1)
            c0 = str_charat(S_PCT + i * 16, 0)
            if c0 != 43 and c0 != 45 and S_PCT100[i] >= 0:
                strcat(S_PB, "+")
            strcat(S_PB, S_PCT + i * 16)
            strcat(S_PB, "%")
            ui_2d_text(SW - pad - 12, y + 8, S_PB, col, 13, VML_ANCHOR_RIGHT)

            # 名称（左，粗）与现价（右，大）
            # The name (left, bold) and the price (right, large)
            ui_2d_text_styled(pad + 12, y + 28, S_NAME + i * 32, C_TITLE, 16, VML_ANCHOR_LEFT,
                              VML_FONT_BOLD)
            ui_2d_text(SW - pad - 12, y + 26, S_PRICE + i * 16, C_TITLE, 18, VML_ANCHOR_RIGHT)

            # 涨跌幅条：中间那根竖线是 0，满格 = ±10%。纯装饰，但一眼看得出谁在动。
            # The change bar: the tick in the middle is zero and a full half is +/-10%.
            ix = pad + 12
            iw = SW - pad * 2 - 24
            if iw < 40:
                iw = 40
            by = y + h - 18
            ui_2d_rect(ix, by, iw, 8, C_TRACK, 1, 0, 4)
            mid = ix + iw // 2
            ui_2d_rect(mid - 1, by - 1, 2, 10, C_DIM, 1, 0, 1)
            amt = S_PCT100[i]
            if amt < 0:
                amt = 0 - amt
            if amt > 1000:
                amt = 1000
            bw = amt * (iw // 2) // 1000
            if bw < 2:
                bw = 2
            if S_PCT100[i] > 0:
                ui_2d_rect(mid, by, bw, 8, C_UP, 1, 0, 2)
            elif S_PCT100[i] < 0:
                ui_2d_rect(mid - bw, by, bw, 8, C_DOWN, 1, 0, 2)

            i = i + 1

        ui_2d_text(cx, SH - 26, "任意键刷新（重新抓一次）   Esc / q 退出" if LANG == 0
                   else "any key refreshes   Esc / q quits", C_DIM, 11, VML_ANCHOR_CENTER)

    ui_2d_present()          # ⚠ 不调这句 = 黑屏
                             # ⚠ without this call you get a black screen


# ── 主流程 ────────────────────────────────────────────
# ── main flow ─────────────────────────────────────────

LANG = ui_scr_get_language()
FIRST = 1
IP = 0
FD = -1
N = 0
WANTED = 0
ROUNDS = 0
CODE = 0
FH = 0
SRC = 0
MODE = 0
FAIL = 0

load_gtab()                  # GB2312 表拼进 GT，只做一次
# build the GB2312 table once

print_str("=== VML 股票行情 ===\n" if LANG == 0 else "=== VML Stock Quotes ===\n")
print_str("主机 " if LANG == 0 else "Host ")
print_str(HOST)
print_str("  请求 " if LANG == 0 else "  query ")
print_str(QUERY)
print_str("\n")

# ① 域名 → IP（宿主不解析域名）
# ① Hostname to IP (the host does not resolve names)
IP = vml_dns_resolve(HOST)
if IP <= 0:
    print_str("[警告] DNS 解析失败（转为离线）\n" if LANG == 0
              else "[WARN] DNS lookup failed (going offline)\n")
    OFFLINE = 1
else:
    print_str("[1/3] DNS  ")
    print_str(IP)
    print_str("\n")

# ②③ 抓数据 —— 建套接字、连接、发请求、收完整响应、剥头、状态码**全在库里**
#   ⚠ 用 `http_get_to` 而不是 `http_get`：后者把「连哪儿」与「Host 写什么」交给**同一个参数**，
#     传域名连不上（#334 不解析域名）、传 IP 则 Host 头成了 IP ⇒ 虚拟主机按 Host 路由返回 **404**。
# The rest of the fetch lives in the library's `http_get_to` (connect to the IP, `Host:` carries the name).
if OFFLINE == 0:
    CODE = http_get_to(IP, PORT, HOST, PATH, RBUF, BUFCAP)
    if CODE != 200:
        print_str("[警告] 抓取失败（转为离线）\n" if LANG == 0
                  else "[WARN] fetch failed (going offline)\n")
        OFFLINE = 1
    else:
        BLEN = strlen(RBUF)          # `http_get_to` 只往回装**正文**（头已经剥掉了）
        # it stores **only the body**; headers are already stripped
        HEAD = 0
        print_str("[3/3] HTTP ")
        print_int(CODE)
        print_str("  正文 " if LANG == 0 else "  body ")
        print_int(BLEN)
        print_str(" 字节\n" if LANG == 0 else " bytes\n")

if OFFLINE == 0:
    LIM = HEAD + BLEN
    N_STK = parse_quote(HEAD, BLEN)
    if N_STK <= 0:
        # 连上了、HTTP 也对，却一条都没解出来 ⇒ 接口改版了，退回本地。
        # Connected with a valid HTTP status, yet nothing parsed -- the feed changed shape.
        print_str("[警告] 一条也没解出来（接口改版？转离线）\n" if LANG == 0
                  else "[WARN] nothing parsed (feed changed? going offline)\n")
        OFFLINE = 1

# ④ 离线：读本地的 stocks.txt。⚠ `mode = 0` 是**只读**，写成 1 会把文件清空再读。
# ④ Offline: read the local stocks.txt. ⚠ `mode = 0` is **read-only**; 1 truncates it first.
if OFFLINE != 0:
    N_STK = 0
    SRC = -1
    I = 0
    while I < 3:
        if I == 0:
            FH = vml_file_open("stocks.txt", 0)
        elif I == 1:
            FH = vml_file_open("examples/python/app/stocks.txt", 0)
        else:
            FH = vml_file_open("Examples/python/app/stocks.txt", 0)
        if FH >= 0:
            SRC = FH
            I = 3
        else:
            I = I + 1
    if SRC < 0:
        print_str("[失败] 离线且找不到 stocks.txt —— 把它放到本程序同目录。\n" if LANG == 0
                  else "[FAIL] offline and stocks.txt is nowhere -- put it next to this program.\n")
        FAIL = 1
    else:
        BLEN = vml_file_read(SRC, RBUF, BUFCAP - 1)
        vml_file_close(SRC)
        if BLEN <= 0:
            print_str("[失败] stocks.txt 是空的\n" if LANG == 0
                      else "[FAIL] stocks.txt is empty\n")
            FAIL = 1
        else:
            memset(RBUF + BLEN, 0, 1)
            LIM = BLEN
            N_STK = parse_local(BLEN)
            print_str("[离线] stocks.txt 解析出 " if LANG == 0
                      else "[offline] stocks.txt yielded ")
            print_int(N_STK)
            print_str(" 只\n" if LANG == 0 else " stocks\n")

if FAIL == 0:
    print_str("解析 " if LANG == 0 else "parsed ")
    print_int(N_STK)
    print_str(" 只：\n" if LANG == 0 else " stocks:\n")
    I = 0
    while I < N_STK:
        print_str("  ")
        print_str(S_CODE + I * 12)
        print_str(" ")
        print_str(S_NAME + I * 32)
        print_str(" ")
        print_str(S_PRICE + I * 16)
        print_str(" ")
        print_str(S_PCT + I * 16)
        print_str("%\n")
        I = I + 1

# ⑤ 开窗 → 画 → 消息循环（任意键重新抓一次）
# ⑤ Open a window, draw, then run the message loop (any other key re-fetches)
# ⚠ 版面那几项是**模块级标量**，赋值只能写在顶层（函数里写就是新建局部量，实测）。
# ⚠ The layout values are **module-level scalars**, so the assignments live at the top level
#   (inside a function they would only create locals, measured).
if FAIL == 0:
    # 开窗时传 0：尺寸交给宿主（用真实可用区）；开窗后再问一次真实画布（见下）。
    # Open with 0 so the host decides the size (the real usable area), then ask the real canvas
    # again below.
    ui_win_open_ex("stock.py", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD)
    SW = scr_w()
    SH = scr_h()
    draw()

    T0 = ui_time_get_ms()
    DONE = 0
    while DONE == 0 and ui_win_is_closed() == 0:
        T = ui_msg_fetch_wait(200)
        if T == VML_MSG_NONE:
            if ui_time_get_ms() - T0 > IDLE_EXIT_MS:
                DONE = 1                 # 空闲超时（验收用）
                # idle timeout (for automated checks)
        elif T == VML_MSG_KEYDOWN:
            K = ui_msg_get_a()
            T0 = ui_time_get_ms()
            if K == VML_KEY_ESCAPE or K == 113 or K == 81:
                DONE = 1
            else:
                # 任意键 = 重新抓一次再刷新
                # any other key re-fetches and redraws
                OFFLINE = 0
                N_STK = 0
                IP = vml_dns_resolve(HOST)
                if IP <= 0:
                    OFFLINE = 1
                else:
                    CODE = http_get_to(IP, PORT, HOST, PATH, RBUF, BUFCAP)
                    if CODE != 200:
                        OFFLINE = 1
                    else:
                        BLEN = strlen(RBUF)      # 库只往回装**正文**
                        HEAD = 0
                        LIM = HEAD + BLEN
                        N_STK = parse_quote(HEAD, BLEN)
                        if N_STK <= 0:
                            OFFLINE = 1
                if OFFLINE != 0:
                    N_STK = 0
                    SRC = -1
                    I = 0
                    while I < 3:
                        if I == 0:
                            FH = vml_file_open("stocks.txt", 0)
                        elif I == 1:
                            FH = vml_file_open("examples/python/app/stocks.txt", 0)
                        else:
                            FH = vml_file_open("Examples/python/app/stocks.txt", 0)
                        if FH >= 0:
                            SRC = FH
                            I = 3
                        else:
                            I = I + 1
                    if SRC >= 0:
                        BLEN = vml_file_read(SRC, RBUF, BUFCAP - 1)
                        vml_file_close(SRC)
                        if BLEN > 0:
                            memset(RBUF + BLEN, 0, 1)
                            LIM = BLEN
                            N_STK = parse_local(BLEN)
                draw()
        elif T == VML_MSG_WINDOWORIENT or T == VML_MSG_WINDOWRESIZE:
            SW = scr_w()
            SH = scr_h()
            draw()
        elif T == VML_MSG_WINDOWCLOSE:
            DONE = 1
