← 都来码 / Dolaima← 例程索引python/app/notepad.py原文

python/app/notepad.py · 28721 字节 · 下载原文

# notepad.py —— 常用小程序④:**记事本**(第 4 层:`ui_*` 图形界面)

# notepad.py -- Everyday utility #4: a **notepad** (layer 4: the `ui_*` graphical interface)

#

# 一扇窗口里的纯文本编辑器:**打字 / 退格 / 回车换行 / 光标可见**,`Esc` **保存并退出**,

# A plain text editor in one window: **type, backspace, Enter for a new line, a visible caret**,

# `Esc` **saves and quits**, and the next run reads the last session back.

# 下次运行把上次的内容读回来。

# The next run reads the last session back.

#

# 这是 `Examples/c/app/notepad.c` 的 Python 移植:版式、键位、存盘时机(**只有循环之后那一处**)、

# This is the Python port of `Examples/c/app/notepad.c`: the layout, the keys and the single place

# that saves (after the loop) are unchanged.

# `note.txt` 这个相对路径,都照搬。

# The relative path `note.txt` is used as is.

#

# ## 正文放在哪:`vml_alloc` 出来的裸内存(**不是 Python 的字符串**)

# ## Where the text lives: raw memory from `vml_alloc` (**not** a Python string)

#

# C 版是一维扁平 `char nbuf[]`。本语言没有数组取地址、没有 `peek`/`poke`(`peekb(x)` 会编成

# The C version uses a flat `char nbuf[]`. This language has no address-of on an array and no

# `peek`/`poke` (`peekb(x)` compiles to `CALL vml_peekb`, and no such label exists under `Lib/` --

# `CALL vml_peekb`,而 `Lib/` 下没有这个标签,实测 `error: 未定义的函数 'vml_peekb'`),

# measured), so the buffer is a **raw address** from `vml_alloc` and both directions borrow the

# 所以缓冲是一块 `vml_alloc` 的**裸地址**,读写各借共享库一件工具:

# shared library:

#   · 读一个字节 → `str_charat(NBUF, i)`(等价于 C 的 `nbuf[i]`);

#   · read one byte -> `str_charat(NBUF, i)` (what C writes as `nbuf[i]`);

#   · 写一个字节 → `memset(NBUF + i, ch, 1)`(等价于 `nbuf[i] = ch`);

#   · write one byte -> `memset(NBUF + i, ch, 1)` (what C writes as `nbuf[i] = ch`);

#   · 取一段 → `str_substr(dst, NBUF, pos, n)`(等价于"把第 i 个字符抄成一个小串")。

#   · take a slice -> `str_substr(dst, NBUF, pos, n)` (the C version's "copy one char into `u`").

# ⚠ `str_charat` 回的是**有符号**字节(实测 `0xE5` 回 -27),所以凡是要跟 `0x80` 比大小时

# ⚠ `str_charat` returns a **signed** byte (measured: `0xE5` comes back as -27), so anything

#   compared against `0x80` has to be masked with `& 255` first -- that is the `& 255` scattered

#   一律先 `& 255`(本文件里那几处 `& 255` 就是为此)。ASCII 的比较(10/13/32..126)不受影响。

#   through this file. Plain ASCII comparisons (10/13/32..126) are unaffected.

# ⚠ `str_substr` 的右边界靠 `strlen(src)` 定,所以读盘之后要补一个 0(`load_file` 里那句)。

# ⚠ `str_substr` bounds itself by `strlen(src)`, so a 0 is written after reading the file

#   (the `memset` in `load_file`).

#

# ## 函数改不了模块级标量 ⇒ 状态改动一律"返回新值、顶层再赋回去"

# ## A function cannot assign a module-level scalar, so every change is "return the new value"

#

# 实测:`def setglob(): G = 42` 之后顶层读 `G` 还是旧值。所以本文件里 `backspace`

# `insert_ch`/

# Measured: after `def setglob(): G = 42` the top level still reads the old `G`. So `backspace`,

# `cursor_left`/`cursor_right`/`strip_cr` 都**返回**新值(`BLEN`/`TCUR` 由顶层赋回去),

# `insert_ch`, `cursor_left`/`cursor_right` and `strip_cr` all **return** their new value (the top

# level assigns it back to `BLEN`/`TCUR`), and `cursor_rc` hands its two results back through the

# 而 `cursor_rc` 两个结果走一个模块级小列表 `RC`(对应 C 版那两个 `int*` 出参)。

# module-level list `RC` (the C version's two `int*` out-parameters).

#

# ## 界面文字跟着宿主语言走

# ## The on-screen text follows the host language

#

# `ui_scr_get_language()`(与 `calendar.py`/`weather.py` 同一套做法)。

# `ui_scr_get_language()`, exactly as `calendar.py`/`weather.py` do it.


# ── 显式引用:`ui_*` 来自共享调用库 `vmlui.vml`;文件/字符串助手来自共享库 ──

# ── explicit references: `ui_*` from the shared call library `vmlui.vml`; the file/string helpers

#    from the shared library ──

import vml_ui
import shared

IDLE_EXIT_MS = 10000        # 空闲这么久就自己退出(例程要能被自动验收)

                            # Idle this long and it quits (samples must be automatically verifiable)

NOTE_CAP = 2048             # 正文缓冲容量(字节)

# Text buffer capacity (bytes)

NOTE_FILE = "note.txt"      # ⚠ 相对路径

# ⚠ A relative path


# 配色(真彩 AARRGGBB,第 4 层没有索引色)

# Colours (true colour AARRGGBB; layer 4 has no palette)

C_BG = 0xFF101018           # 窗口底

# window background

C_PAPER = 0xFF1A1A24        # 正文"纸"面

# the sheet the text sits on

C_TITLE = 0xFFE8E8F0
C_TEXT = 0xFFD8DCE8
C_DIM = 0xFF9AA0B0
C_CUR = 0xFFFFC64A          # 光标(黄色竖条)

# the caret (a yellow bar)

C_HINT = 0xFF7A8090

# ── 正文缓冲与暂存串 ──────────────────────────────────

# ── the text buffer and the scratch strings ───────────

NBUF = vml_alloc(NOTE_CAP)  # 正文;**不保证以 0 结尾**(长度看 `BLEN`)

                            # The text; **not guaranteed NUL-terminated** (the length is `BLEN`)

S_NUM = vml_alloc(16)       # `itoa` 的落点

# where `itoa` writes

S_TB = vml_alloc(64)        # 标题栏与状态行

# the title bar and the status line

S_U = vml_alloc(8)          # 一个 UTF-8 字符的暂存

# one UTF-8 character, staged


# 已用字节数 / 光标(字节下标 0..BLEN) / 启动时有没有读到文件(1 = 读到了)

# Bytes in use / the caret (a byte index 0..BLEN) / whether the file was read at startup

BLEN = 0
TCUR = 0
TLOADED = 0
LANG = 0

# `cursor_rc` 的两个出参:光标所在的显示行、显示列(C 版是 `int* orow

# ocol`)

# The two out-parameters of `cursor_rc`: the caret's display row and column (C used `int*`)

RC = [0, 0]


# ── 小工具 ────────────────────────────────────────────

# ── small helpers ─────────────────────────────────────


# 这个字节起头是几个字节的 UTF-8 序列(认不出按 1 算)

# How many bytes the UTF-8 sequence starting here is (unrecognized counts as 1)

def utf8_len(lead):
    if lead < 0x80:
        return 1
    if (lead & 0xE0) == 0xC0:
        return 2
    if (lead & 0xF0) == 0xE0:
        return 3
    if (lead & 0xF8) == 0xF0:
        return 4
    return 1


# 这个字占几格:ASCII 与 2 字节的拉丁扩展各 1 格,CJK/emoji(3、4 字节)占 2 格 —— 全角

# How many cells this character takes: ASCII and 2-byte Latin take 1, CJK/emoji (3- and 4-byte) 2

def cell_width(lead):
    if lead < 0x80:
        return 1
    if (lead & 0xE0) == 0xC0:
        return 1
    return 2


# 从第 `c` 格再放一个占 `w` 格的字放得下吗?放不下就要先换行。

# Does one more character of `w` cells fit starting at column `c`? If not, wrap first.

# ⚠ **画正文与定位光标必须共用这一处** —— 各写一套,光标就会"停在行尾而字掉到下一行"。

# ⚠ **Drawing and the caret must share this one place** -- two copies and the caret "sits at the

#   end of the line while the character lands at the start of the next" (the C file header's ③).

def wraps(c, w, cols):
    if c + w > cols:
        return 1
    return 0


# 光标所在的**显示行/列**(走一遍正文得到;`\n` 与软换行都算)—— 结果放 `RC`

# The **display row/column** of the caret (found by walking the text; `\n` and soft wraps both

# count) -- the result goes into `RC`

def cursor_rc(idx, blen, cols):
    r = 0
    c = 0
    i = 0
    ch = 0
    n = 0
    w = 0
    while i < idx:
        ch = str_charat(NBUF, i) & 255
        if ch == 10:
            r = r + 1
            c = 0
            i = i + 1
        else:
            n = utf8_len(ch)
            if i + n > blen:
                n = 1                  # 缓冲尾上只剩半个序列(不该发生)

                # half a sequence at the tail

            w = cell_width(ch)
            if wraps(c, w, cols) == 1:
                r = r + 1
                c = 0
            c = c + w
            i = i + n
    # 正好落在一行末尾那一格之外 ⇒ 按下一行行首算 —— 与"再打一个字会被放到哪儿"同一个判据

    # Exactly past the last cell of a row means the next row's start -- the same judge as "where

    # would the next character go"

    if wraps(c, 1, cols) == 1:
        r = r + 1
        c = 0
    RC[0] = r
    RC[1] = c


# 行数与字数(只给标题栏看)

# Line and character counts (for the title bar only)

def count_lines(blen):
    i = 0
    n = 1
    while i < blen:
        if (str_charat(NBUF, i) & 255) == 10:
            n = n + 1
        i = i + 1
    return n


def count_chars(blen):
    i = 0
    n = 0
    ch = 0
    while i < blen:
        ch = str_charat(NBUF, i) & 255
        i = i + utf8_len(ch)
        if ch != 10:
            n = n + 1                  # 换行不算"字"

            # a line break is not a "character"

    return n


# `\r` 一律清掉:本程序自己写的是 LF,可**别人**(Windows 记事本、Git 的 `core.autocrlf`)留下的

# Drop every `\r`: this program writes LF, but files left by **someone else** (Windows Notepad, or

# 文件是 CRLF。`\r` **看不见,但在这个"一个字一格"的模型里照样占掉一格**。

# Git's `core.autocrlf`) are CRLF, and a `\r` **is invisible yet still takes a cell** in this

# 写回去时统一成 LF。返回新的长度。

# one-character-per-cell model. Writes go back out as LF only. Returns the new length.

def strip_cr(blen):
    i = 0
    o = 0
    ch = 0
    while i < blen:
        ch = str_charat(NBUF, i) & 255
        if ch == 13:
            if i + 1 < blen:
                if (str_charat(NBUF, i + 1) & 255) == 10:
                    i = i + 1              # CRLF ⇒ 丢掉 CR,下一轮把 LF 原样抄过去

                    # CRLF -> drop the CR; the next round copies the LF through.

                    continue               # CRLF -> drop the CR; next round copies the LF

            memset(NBUF + o, 10, 1)        # 光秃秃的 CR ⇒ 当成换行

            # a bare CR becomes a line break

            o = o + 1
            i = i + 1
        else:
            memset(NBUF + o, ch, 1)
            o = o + 1
            i = i + 1
    return o


# ── 文件(⚠ 读用 0、写用 1)───────────────────────────

# ── files (⚠ read = 0, write = 1) ─────────────────────


# 读回来。返回**字节数**(`BLEN`/`TCUR` 由顶层赋)或 -1 = "没有这个文件"。

# Reads it back. Returns the **byte count** (the top level assigns `BLEN`/`TCUR`), or -1 for

# "no such file" (the document then stays empty).

# ⚠ `mode = 0` 是**只读**:写成 1 会把文件清空再读,拿到一份空数据。

# ⚠ `mode = 0` is **read-only**: passing 1 would truncate the file first and hand back nothing.

def load_file():
    fh = 0
    n = 0
    fh = vml_file_open(NOTE_FILE, 0)
    if fh < 0:
        return -1                   # 错误码是**负数**(-200 文件不存在 / -202 越界)

                                    # a failing call returns a **negative** code (-200 / -202)

    n = vml_file_read(fh, NBUF, NOTE_CAP - 1)
    vml_file_close(fh)
    if n < 0:
        n = 0
    memset(NBUF + n, 0, 1)          # 存盘时不用它,但 `str_substr`/`strlen` 要靠它定边界

                                    # not needed for saving, but `str_substr`/`strlen` need it

    return strip_cr(n)              # ⚠ `\r` 看不见却占一格,光标会浮在文字右边

                                    # ⚠ a `\r` is invisible yet takes a cell



# 写回去。返回写成功的字节数(失败返回 -1)。

# Writes it back. Returns the bytes written, or -1.

# ⚠ `mode = 1` 是 `FileMode.Create`(**先截断**)—— 所以我们才要整份重写。

# ⚠ `mode = 1` is `FileMode.Create` (**truncating**) -- hence the full rewrite.

def save_file(blen):
    fh = 0
    w = 0
    fh = vml_file_open(NOTE_FILE, 1)
    if fh < 0:
        return -1
    w = vml_file_write(fh, NBUF, blen)
    vml_file_close(fh)
    if w != blen:
        return -1
    return w


# ── 编辑(都**返回**新值,由顶层赋回 `BLEN`

# `TCUR`)──

# ── editing (each **returns** the new value; the top level assigns it back) ──


# 在光标处插一个字节(可打印 ASCII 或 `\n`)。返回新的光标;缓冲满由调用方先判。

# Inserts one byte at the caret (printable ASCII or `\n`). Returns the new caret; a full buffer is

# the caller's business (it checks first and drops the key).

def insert_ch(ch, tcur, blen):
    i = blen
    while i > tcur:
        memset(NBUF + i, str_charat(NBUF, i - 1), 1)
        i = i - 1
    memset(NBUF + tcur, ch, 1)
    return tcur + 1


# 退格:删掉光标**前一个字符**,返回新的光标。⚠ 要跨过**整个 UTF-8 序列** —— 只退一个字节会把

# Backspace: delete the character **before** the caret and return the new caret. ⚠ It has to step

# 一个汉字劈成"前导字节 + 半个身子",屏幕上是乱码、存盘后文件里也是坏字节(实测)。

# over the **whole UTF-8 sequence** -- one byte would split a CJK character in half (measured).

def backspace(tcur, blen):
    start = 0
    n = 0
    i = 0
    if tcur <= 0:
        return 0
    start = tcur - 1
    while start > 0:
        if ((str_charat(NBUF, start) & 255) & 192) != 128:
            break
        start = start - 1
    n = tcur - start
    i = start
    while i + n < blen:
        memset(NBUF + i, str_charat(NBUF, i + n), 1)
        i = i + 1
    return start


# 光标左移一个**字符**(不是一字节,同理)

# Move the caret one **character** left (not one byte, same reason)

def cursor_left(tcur):
    i = 0
    if tcur <= 0:
        return 0
    i = tcur - 1
    while i > 0:
        if ((str_charat(NBUF, i) & 255) & 192) != 128:
            break
        i = i - 1
    return i


# 光标右移一个字符

# Move the caret one character right

def cursor_right(tcur, blen):
    if tcur >= blen:
        return tcur
    return tcur + utf8_len(str_charat(NBUF, tcur) & 255)


# ── 版面与绘制 ────────────────────────────────────────

# ── layout and drawing ────────────────────────────────


def scr_w():
    w = ui_scr_get_w()
    if w <= 0:
        w = 480             # 宿主答不上来时的兜底(开窗时传 0,尺寸由宿主自己定)

                            # fallback when the host cannot answer (the window is opened with 0 and the size is the host's to decide)

    return w


def scr_h():
    h = ui_scr_get_h()
    if h <= 0:
        h = 640
    return h


def draw():
    # 版面**整段算在 draw 里**:函数改不了模块级标量(实测),而这些量除了本函数没人用 ——

    # The whole layout is computed **inside `draw`**: a function cannot assign module-level scalars

    # 正好与 C 版 `layout(); draw();` 那对调用等价,画布一变(旋转/缩放)重画就跟着重算。

    # (measured), and nothing outside this function needs them -- which is equivalent to the C

    # version's `layout(); draw();` pair, so a canvas change re-derives everything on redraw.

    sw = scr_w()
    sh = scr_h()
    lang = LANG
    blen = BLEN
    tcur = TCUR
    pad = 0
    usable = 0
    body = 0
    fs = 0
    cw = 0
    lh = 0
    x0 = 0
    y0 = 0
    cols = 0
    rows = 0
    gtop = 0
    crow = 0
    ccol = 0
    r = 0
    c = 0
    i = 0
    n = 0
    w = 0
    ch = 0
    y = 0
    nl = 0
    nc = 0
    ink = 0
    ptop = 0
    pbot = 0
    bar = 0

    pad = sw // 20
    if pad < 8:
        pad = 8

    fs = sw // 30              # 字号随宽度走:手机窄屏小一点,桌面大一点

    # The font follows the width: smaller on a narrow phone, larger on the desktop.

    if fs < 11:
        fs = 11
    if fs > 17:
        fs = 17
    cw = fs + 2                # 一格宽 = 字高 + 2px 字距

    # one cell = the font height plus 2px

    lh = fs + 7                # 行高

    # line height


    y0 = pad + 34              # 标题栏占掉上面一条

    # the title bar takes the strip above

    usable = sw - pad * 2
    cols = usable // cw
    if cols < 8:
        cols = 8
    if cols > 40:              # 宽窗口下别把一行拉得太长

    # do not stretch one line forever

        cols = 40
    x0 = (sw - cols * cw) // 2     # 整块水平居中

    # centre the block horizontally

    if x0 < pad:
        x0 = pad

    body = sh - pad - 46 - y0      # 底下给两行提示留位置

    # two hint lines get the bottom strip

    if body < lh * 3:
        body = lh * 3
    rows = body // lh
    if rows < 1:
        rows = 1

    # ① 先算光标在哪一格 —— 滚动要照着它来,所以**必须在画之前**算好

    # ① Work out the caret cell **before drawing**: scrolling follows it

    cursor_rc(tcur, blen, cols)
    crow = RC[0]
    ccol = RC[1]
    gtop = 0
    if crow >= rows:
        gtop = crow - rows + 1

    ui_2d_clear(C_BG)

    # ② 标题栏:左边文件名,右边行数 / 字数

    # ② Title bar: the file name on the left, line and character counts on the right

    ui_2d_text_styled(pad, pad, NOTE_FILE, C_TITLE, fs + 3, VML_ANCHOR_LEFT, VML_FONT_BOLD)
    nl = count_lines(blen)
    nc = count_chars(blen)
    itoa(nl, S_NUM)
    strcpy(S_TB, S_NUM)
    strcat(S_TB, " 行 · " if lang == 0 else " lines · ")
    itoa(nc, S_NUM)
    strcat(S_TB, S_NUM)
    strcat(S_TB, " 字" if lang == 0 else " chars")
    ui_2d_text(sw - pad, pad + 4, S_TB, C_DIM, fs - 2, VML_ANCHOR_RIGHT)

    # ③ 正文:一块"纸"+ 逐字上格

    # ③ The body: a sheet of "paper" plus one character per cell

    # ⚠ `ui_2d_text` 给的 y **不是"字盒的顶"而是贴着基线**(实测:字号 16、y=58 时,拉丁字母的

    # ⚠ The y given to `ui_2d_text` is **not the top of the glyph box but the baseline** (measured:

    #   墨迹在 48..57、汉字在 47..59 —— 都在 y 的**上方**)⇒ 纸面与光标都要按"墨迹高出多少"往上让,

    #   at size 16 with y=58 the Latin ink sits at 48..57 and CJK ink at 47..59, both **above** y),

    #   不然光标会画在自己那一行的下面(看着像"光标跳到了下一行",实测过)。

    #   so both the sheet and the caret move up by the overhang; otherwise the caret is drawn below

    #   its own line (it reads as if the caret had jumped a line -- measured).

    ink = (fs * 3) // 4                  # 墨迹高出 y 的量(字号 16 ⇒ 12)

    # the overhang

    ptop = y0 - ink - 5
    pbot = y0 + (rows - 1) * lh + fs + 6
    ui_2d_rect(x0 - 6, ptop, cols * cw + 12, pbot - ptop, C_PAPER, 1, 0, 8)

    r = 0
    c = 0
    i = 0
    while i < blen:
        ch = str_charat(NBUF, i) & 255
        if ch == 10:
            r = r + 1
            c = 0
            i = i + 1
        else:
            # ⚠ **按码点切,不按字节切**:一个汉字是 3 个字节,逐字节画出来就是 3 个乱码格。

            # ⚠ **Cut by code point, never by byte**: a CJK character is 3 bytes, so drawing byte

            #   by byte yields three mojibake boxes (measured: three cells of garbage).

            # (measured: the three bytes of one CJK character became three garbage cells).

            n = utf8_len(ch)
            if i + n > blen:
                n = 1                     # 末尾只剩半个序列(不该发生)

                # half a sequence at the tail

            w = cell_width(ch)
            if wraps(c, w, cols) == 1:
                r = r + 1
                c = 0
            if r >= gtop and r < gtop + rows:
                y = y0 + (r - gtop) * lh
                str_substr(S_U, NBUF, i, n)
                ui_2d_text(x0 + c * cw, y, S_U, C_TEXT, fs, VML_ANCHOR_LEFT)
            c = c + w
            i = i + n

    # ④ 空文档给一行欢迎语(**只是占位,不进 `NBUF`** —— 否则第一次退出就会把欢迎语当成用户的

    # ④ An empty document shows a welcome line (**a placeholder only, never put into `NBUF`** --

    #   内容存进 `note.txt`)。

    #   otherwise the first exit would save the welcome line into `note.txt` as the user's text).

    if blen == 0:
        ui_2d_text(x0, y0, "空白笔记 —— 直接打字" if lang == 0 else "empty note - just type",
                   C_DIM, fs, VML_ANCHOR_LEFT)
        ui_2d_text(x0, y0 + lh, "Esc 保存并退出" if lang == 0 else "Esc saves and quits",
                   C_DIM, fs, VML_ANCHOR_LEFT)

    # ⑤ 光标:一条竖条,画在**当前字符位置**上(正好在一行末尾时它会落在下一行行首)

    # ⑤ The caret: a vertical bar drawn **at the current character position** (at the end of a full

    #    row it lands at the start of the next one).

    if crow >= gtop and crow < gtop + rows:
        bar = fs // 6
        if bar < 2:
            bar = 2
        if bar > 4:
            bar = 4
        y = y0 + (crow - gtop) * lh
        ui_2d_rect(x0 + ccol * cw, y - ink + 1, bar, fs + 3, C_CUR, 1, 0, 0)

    # ⑥ 底部:操作提示 + 文件状态(第二次启动一眼能看出"上次存的内容读回来了")

    # ⑥ Bottom: the controls and the file state (a second run shows at a glance that the last

    #    session was read back).

    ui_2d_text(pad, sh - pad - 32,
               "回车 换行 · 退格 删 · Esc 保存并退出" if lang == 0
               else "Enter newline · Backspace delete · Esc save & quit",
               C_HINT, fs - 3, VML_ANCHOR_LEFT)
    strcpy(S_TB, "文件 " if lang == 0 else "file ")
    strcat(S_TB, NOTE_FILE)
    if TLOADED != 0:
        strcat(S_TB, " 已读取 " if lang == 0 else " loaded ")
        itoa(blen, S_NUM)
        strcat(S_TB, S_NUM)
        strcat(S_TB, " 字节" if lang == 0 else " bytes")
    else:
        strcat(S_TB, " 新建(退出时写入)" if lang == 0 else " new (written on exit)")
    ui_2d_text(pad, sh - pad - 14, S_TB, C_HINT, fs - 3, VML_ANCHOR_LEFT)

    ui_2d_present()          # ⚠ 不调这句 = 黑屏

                             # ⚠ without this call you get a black screen



# ── 主流程 ────────────────────────────────────────────

# ── main flow ─────────────────────────────────────────


LANG = ui_scr_get_language()

# 开窗时传 0:尺寸交给宿主(用真实可用区)。`draw` 开窗后会自己再问一次真实画布 ——

# 开窗前问到的只是估算值,不能拿它排版。

# Open with 0 so the host decides the size (the real usable area). `draw` asks for the real canvas

# again after opening -- the pre-open value is only an estimate and must not drive layout.

ui_win_open_ex("notepad.py", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD)

N = load_file()
if N < 0:
    BLEN = 0                     # 没有文件 = 空文档 + 一行欢迎语(见 `draw` ④)

    # no file = an empty document plus a welcome line (see point 4 inside `draw`).

    TCUR = 0                     # no file = an empty document plus a welcome line (see `draw` 4)

    TLOADED = 0
else:
    BLEN = N
    TLOADED = 1
    TCUR = BLEN                  # 光标停在上次内容的末尾,接着往下打就是追加

                                 # the caret sits at the end, so typing continues where it left off

print_str("[notepad] ")
print_str(NOTE_FILE)
if TLOADED != 0:
    print_str(" 读到 " if LANG == 0 else " loaded ")
    print_int(BLEN)
    print_str(" 字节\n" if LANG == 0 else " bytes\n")
else:
    print_str(" 还没有,从空文档开始(内容只能是这个程序的输入)\n" if LANG == 0
              else " not found, starting empty (content is only what you type here)\n")

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()
        # ⚠ **顺序是有讲究的:方向键那一支必须在"可打印 ASCII"之前** ——

        # ⚠ **The order matters: the arrow-key arms must come before the "printable ASCII" one** --

        #   `VML_KEY_LEFT/UP/RIGHT/DOWN` = 37/38/39/40,**全都落在 32..126 里**,先判可打印的话,

        #   they are 37/38/39/40, **all inside 32..126**, so testing the printable range first turns

        #   按一下左箭头会往正文里**插一个 `%`**(实测:连按三下左箭头,存下来的文件里多了 3 个

        #   one Left press into **a literal `%` in the text** (measured: three Left presses left

        #   `%`,光标一步没动)。

        #   three `%` in the saved file and moved the caret not at all).

        if K == VML_KEY_ESCAPE:
            DONE = 1                                  # Esc = 保存并退出(保存统一在循环外做)

                                                      # Esc = save and quit (the save is after the loop)

        elif K == VML_KEY_BACKSPACE:
            NT = backspace(TCUR, BLEN)
            BLEN = BLEN - (TCUR - NT)
            TCUR = NT
            draw()
        elif K == VML_KEY_ENTER:
            if BLEN < NOTE_CAP - 1:
                TCUR = insert_ch(10, TCUR, BLEN)      # ⚠ 回车要变成缓冲里的 10,见文件头 ④

                # ⚠ Enter becomes a 10 in the buffer (pitfall 4 in the header).

                BLEN = BLEN + 1                       # ⚠ Enter becomes a 10 in the buffer

            draw()
        elif K == VML_KEY_LEFT:
            TCUR = cursor_left(TCUR)
            draw()
        elif K == VML_KEY_RIGHT:
            TCUR = cursor_right(TCUR, BLEN)
            draw()
        elif K >= 32 and K <= 126:
            if BLEN < NOTE_CAP - 1:                   # 缓冲满了就丢掉(记事本不该因为打满而崩)

            # a full buffer drops the key (a notepad must not fall over when it fills up).

                TCUR = insert_ch(K, TCUR, BLEN)       # a full buffer drops the key

                BLEN = BLEN + 1
            draw()
    elif T == VML_MSG_WINDOWORIENT or T == VML_MSG_WINDOWRESIZE:
        draw()
    elif T == VML_MSG_WINDOWCLOSE:
        DONE = 1

# ⚠ **收尾只有这一处**:`Esc`、空闲 10 秒、用户点了窗口的返回箭头,三条路都从这里出去。

# ⚠ **This is the only exit**: `Esc`, ten idle seconds and the window's back arrow all leave here.

#   把保存写进 `Esc` 那一支 = 另外两条路悄悄丢内容。

#   Writing the save inside the `Esc` arm would silently lose the text on the other two (header 5).

WROTE = save_file(BLEN)
print_str("[notepad] 写入 " if LANG == 0 else "[notepad] wrote ")
print_str(NOTE_FILE)
print_str(" ")
if WROTE < 0:
    print_int(0)
else:
    print_int(WROTE)
print_str(" 字节,退出\n" if LANG == 0 else " bytes, quitting\n")