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")