c/app/clipboard.c · 16934 字节 · 下载原文
/* clipboard.c —— 常用小程序⑨:**剪贴板小工具**(第 4 层:`ui_*` 图形界面)
* clipboard.c -- Everyday utility #9: a **clipboard toolbox** (layer 4: the `ui_*` graphical interface)
*
* 把剪贴板里的文字拿来处理:读进来 → 按一下按钮做一种变换(转大写 / 转小写 / 去空行 / 去每行首尾空格)
* Take the text from the clipboard and work on it: read it in, tap a button to transform it (upper case,
* → **结果自动写回剪贴板**,接着在别处粘贴即可。底部实时显示统计(字节 / 行 / 空行)。
* lower case, drop blank lines, trim each line) and the result is **written back to the clipboard**
* so you can paste it somewhere else. A live count (bytes / lines / blanks) sits at the bottom.
*
* ## 剪贴板这一端没有怎么办(**桌面指令行宿主实测就没有**)
* ## What happens where there is no clipboard (**the desktop command-line host has none, measured**)
*
* `ui_clipboard_get` 返回 **-1**,`ui_clipboard_set` 返回 **0**。按本仓既定的口径,
* `ui_clipboard_get` returns **-1** and `ui_clipboard_set` returns **0**. By this repo's rule,
* **0 不是错误,是"这一端没有这个能力"** —— 所以本程序:
* **0 is not an error, it means "this end lacks the capability"** -- so this program:
* · 读不到就装一段**内置样例文本**,四种变换照样能按、能看(不然在桌面上就是一片空白);
* · falls back to a **built-in sample text** so all four transforms still work and are visible
* (otherwise the desktop end would just be blank);
* · 写回失败就在状态行写"本机不支持写剪贴板",**不报错、不退出**。
* · says "this machine cannot write the clipboard" in the status line when the write fails, and
* **does not error out or quit**.
*
* ## 第 4 层的规矩(与 `calendar.c` / `stock.c` 同)
* ## Layer-4 rules (same as `calendar.c` / `stock.c`)
*
* 尺寸问宿主、真彩 `0xAARRGGBB`、**画完必须 `ui_2d_present()`**、坐标不写死、循环必须有界
* Ask the host for the size, use true colour `0xAARRGGBB`, **always end with `ui_2d_present()`**, never
* (空闲 ~10 秒自动退出,例程要能被自动验收)。
* hard-code coordinates, and keep the loop bounded (it exits after ~10 s of idling so the sample can be
* checked automatically).
*/
#include <vml_ui.h>
#include <stdlib.h> /* itoa --- digits to text via the library */
#define IDLE_EXIT_MS 10000
#define TEXT_CAP 512 /* 正文容量(字节)。够演示,也不是"内存里的无底洞" */
/* Body capacity in bytes: enough for a demo without being a bottomless pit */
#define NBUF 24
#define NB 5 /* 按钮个数 */
/* number of buttons */
static int SW;
static int SH;
static int lang;
static char text[TEXT_CAP];
static int tlen;
static char status[64]; /* 状态行文字 */
/* the status-line text */
static int status_kind;
/* 0 普通 / 1 成功 / 2 提示 */
/* 0 normal, 1 ok, 2 notice */
#define C_BG 0xFF0E1116
#define C_CARD 0xFF1A1F28
#define C_TITLE 0xFFE8E8F0
#define C_DIM 0xFF9AA0B0
#define C_OK 0xFF51E86E
#define C_WARN 0xFFE0A040
#define C_BTN 0xFF2A3550
#define C_BTN2 0xFF3A4A6A
/* 整数 → 十进制串(`ui_2d_text` 只吃字符串)*/
/* Integer to a decimal string (`ui_2d_text` takes strings) */
static char numbuf[NBUF];
static char *numstr(int v) {
itoa(v, numbuf);
return numbuf;
}
/* UTF-8 首字节 → 这个字符占几个字节(非法首字节按 1 算,宁可少算也不越界)
* UTF-8 lead byte -> how many bytes this character takes (an invalid lead byte counts as 1; better to
* under-count than to run off the end) */
static int utf8_len(char c) {
unsigned char u = (unsigned char)c;
if (u < 0x80) return 1;
if ((u & 0xE0) == 0xC0) return 2;
if ((u & 0xF0) == 0xE0) return 3;
if ((u & 0xF8) == 0xF0) return 4;
return 1;
}
static void set_status(char *s, int kind) {
int i = 0;
while (s[i] != 0 && i < 62) { status[i] = s[i]; i = i + 1; }
status[i] = 0;
status_kind = kind;
}
/* 统计:字节数、行数、空行数 */
/* Count bytes, lines and blank lines */
static void stats(int *lines, int *blanks) {
int i = 0;
int l = 1;
int b = 0;
int line_has = 0;
if (tlen == 0) { *lines = 0; *blanks = 0; return; }
while (i < tlen) {
if (text[i] == 10) {
if (line_has == 0) b = b + 1;
l = l + 1;
line_has = 0;
} else {
line_has = 1;
}
i = i + 1;
}
if (line_has == 0) b = b + 1; /* 结尾那行也是空的(含只以换行结尾的情形)*/
/* the trailing line counts as blank too (including a trailing newline) */
*lines = l;
*blanks = b;
}
/* ① 转大写 */
/* 1. upper case */
static void to_upper(void) {
int i = 0;
while (i < tlen) {
if (text[i] >= 'a' && text[i] <= 'z') text[i] = (char)(text[i] - 32);
i = i + 1;
}
}
/* ② 转小写 */
/* 2. lower case */
static void to_lower(void) {
int i = 0;
while (i < tlen) {
if (text[i] >= 'A' && text[i] <= 'Z') text[i] = (char)(text[i] + 32);
i = i + 1;
}
}
/* ③ 去空行(只留非空行,末尾补一个换行)*/
/* 3. drop blank lines (keep non-blank ones, end with one newline) */
static void drop_blanks(void) {
char out[TEXT_CAP];
int i = 0;
int o = 0;
int line_has = 0;
int line_start = 0;
while (i <= tlen) {
if (i == tlen || text[i] == 10) {
if (line_has != 0) {
int k = line_start;
while (k < i && o < TEXT_CAP - 2) { out[o] = text[k]; o = o + 1; k = k + 1; }
out[o] = 10;
o = o + 1;
}
line_has = 0;
line_start = i + 1;
} else if (text[i] != 13) {
line_has = 1;
}
i = i + 1;
}
i = 0;
while (i < o) { text[i] = out[i]; i = i + 1; }
tlen = o;
}
/* ④ 去每行首尾空格 */
/* 4. trim spaces at both ends of every line */
static void trim_lines(void) {
char out[TEXT_CAP];
int i = 0;
int o = 0;
int s = 0;
int e = 0;
while (i <= tlen) {
if (i == tlen || text[i] == 10) {
e = i;
while (s < e && (text[s] == ' ' || text[s] == 9)) s = s + 1;
while (e > s && (text[e - 1] == ' ' || text[e - 1] == 9 || text[e - 1] == 13)) e = e - 1;
while (s < e && o < TEXT_CAP - 2) { out[o] = text[s]; o = o + 1; s = s + 1; }
if (i < tlen) { out[o] = 10; o = o + 1; }
s = i + 1;
}
i = i + 1;
}
i = 0;
while (i < o) { text[i] = out[i]; i = i + 1; }
tlen = o;
}
/* 结果写回剪贴板;这一端没有就只更新状态行(**0 不是错误**)
* Write the result back to the clipboard; on an end without one, only update the status line (**0 is not an error**) */
static void push_clipboard(void) {
text[tlen] = 0;
if (ui_clipboard_set(text) != 0) {
set_status(lang == 0 ? "已写回剪贴板" : "copied back to the clipboard", 1);
} else {
set_status(lang == 0 ? "本机不支持写剪贴板(桌面指令行宿主就是这种)"
: "this machine cannot write the clipboard (the CLI host is one)", 2);
}
}
static char btn_name(int i) {
if (i == 0) return lang == 0 ? "转大写" : "UPPER";
if (i == 1) return lang == 0 ? "转小写" : "lower";
if (i == 2) return lang == 0 ? "去空行" : "no blanks";
if (i == 3) return lang == 0 ? "去首尾空格" : "trim";
return lang == 0 ? "重新读剪贴板" : "re-read";
}
static void apply(int i) {
if (i == 0) { to_upper(); set_status(lang == 0 ? "已转大写" : "upper case applied", 1); }
else if (i == 1) { to_lower(); set_status(lang == 0 ? "已转小写" : "lower case applied", 1); }
else if (i == 2) { drop_blanks(); set_status(lang == 0 ? "已去空行" : "blank lines dropped", 1); }
else if (i == 3) { trim_lines(); set_status(lang == 0 ? "已去首尾空格" : "lines trimmed", 1); }
else { set_status(lang == 0 ? "重新读了一次" : "re-read", 0); }
if (i != 4) push_clipboard();
}
/* 按钮几何:**画与命中判定共用这一份**(分家就会出现"看着点这个、实际按那个")
* Button geometry: **one copy shared by drawing and hit testing** (split them and taps land on the wrong button) */
static int btn_x(int i, int bw) { return 10 + (i % 3) * (bw + 8); }
static int btn_y(int i) { return SH - 150 + (i / 3) * 46; }
static int btn_w(int i, int bw) { return (i == 4) ? bw * 2 + 8 : bw; }
static void draw(void) {
int i;
int lines;
int blanks;
int bw;
int prev_y;
int prev_h;
char st[80];
ui_2d_clear(C_BG);
ui_2d_text_styled(SW / 2, 14, lang == 0 ? "剪贴板小工具" : "Clipboard toolbox",
C_TITLE, 17, VML_ANCHOR_CENTER, VML_FONT_BOLD);
ui_2d_text(SW / 2, 38, lang == 0 ? "点按钮做变换,结果自动写回剪贴板"
: "tap a button to transform; the result goes back to the clipboard",
C_DIM, 11, VML_ANCHOR_CENTER);
/* 正文预览:按行画,超过预览高度就不再往下画(`ui_2d_text` 不会替你裁剪)
* Body preview: one line per row; stop once the preview is full (`ui_2d_text` will not clip for you) */
prev_y = 64;
prev_h = SH - 64 - 170;
if (prev_h < 80) prev_h = 80;
ui_2d_rect(8, prev_y, SW - 16, prev_h, C_CARD, 1, 0, 8);
{
int i2 = 0;
int row = 0;
int col = 0;
char line[64];
int maxrows = (prev_h - 16) / 18;
int cells = 0;
while (i2 <= tlen && row < maxrows) {
if (i2 == tlen || text[i2] == 10) {
line[col] = 0;
ui_2d_text(18, prev_y + 8 + row * 18, line, C_TITLE, 12, VML_ANCHOR_LEFT);
row = row + 1;
col = 0;
cells = 0;
} else {
/* ⚠ **按码点搬,不按字节裁**(本仓铁律:字符串截断必须按码点)——
* ⚠ **Copy whole code points, never cut by byte** (this repo's rule: truncate by code point) --
* 从中间切开一个汉字(UTF-8 三字节)会让整串变成非法 UTF-8,宿主随即退回 CP437 逐字节画,
* splitting a CJK glyph (3 bytes in UTF-8) makes the whole string invalid UTF-8, the host falls
* 屏幕上就是一排 `û □ τ` 乱码盒子(实测:第一行 44 字节、裁在 40 就中招)。
* back to drawing it byte by byte as CP437 and you get a row of `u-box-tau` garbage boxes
* (measured: the first line is 44 bytes and cutting at 40 triggers exactly that).
*/
int n = utf8_len(text[i2]);
int w = (n == 1) ? 1 : 2; /* 汉字按 2 格算 */
/* a CJK glyph takes two cells */
int k2;
if (cells + w <= 40 && col + n < 60) {
k2 = 0;
while (k2 < n && i2 + k2 < tlen) { line[col] = text[i2 + k2]; col = col + 1; k2 = k2 + 1; }
cells = cells + w;
i2 = i2 + n - 1;
}
}
i2 = i2 + 1;
}
}
/* 统计 */
/* Counts */
stats(&lines, &blanks);
st[0] = 0;
{
int k = 0;
char *p;
p = lang == 0 ? "字节 " : "bytes ";
while (p[k] != 0) { st[k] = p[k]; k = k + 1; }
{
char *nv = numstr(tlen);
int j = 0;
while (nv[j] != 0) { st[k] = nv[j]; k = k + 1; j = j + 1; }
}
p = lang == 0 ? " 行 " : " lines ";
i = 0;
while (p[i] != 0) { st[k] = p[i]; k = k + 1; i = i + 1; }
{
char *nv = numstr(lines);
int j = 0;
while (nv[j] != 0) { st[k] = nv[j]; k = k + 1; j = j + 1; }
}
p = lang == 0 ? " 空行 " : " blank ";
i = 0;
while (p[i] != 0) { st[k] = p[i]; k = k + 1; i = i + 1; }
{
char *nv = numstr(blanks);
int j = 0;
while (nv[j] != 0) { st[k] = nv[j]; k = k + 1; j = j + 1; }
}
st[k] = 0;
}
ui_2d_text(12, prev_y + prev_h + 6, st, C_DIM, 11, VML_ANCHOR_LEFT);
/* 按钮 */
/* Buttons */
bw = (SW - 20 - 16) / 3;
if (bw < 60) bw = 60;
for (i = 0; i < NB; i++) {
int x = btn_x(i, bw);
int y = btn_y(i);
int w = btn_w(i, bw);
ui_2d_rect(x, y, w, 38, (i == 4) ? C_BTN2 : C_BTN, 1, 0, 8);
ui_2d_text(x + w / 2, y + 11, btn_name(i), C_TITLE, 13, VML_ANCHOR_CENTER);
}
/* 状态行 */
/* Status line */
ui_2d_text(SW / 2, SH - 46, status,
status_kind == 1 ? C_OK : (status_kind == 2 ? C_WARN : C_DIM), 12, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, SH - 24, lang == 0 ? "Esc / q 退出(空闲 10 秒也会自己退出)"
: "Esc / q to quit (it also exits after 10 s idle)",
C_DIM, 11, VML_ANCHOR_CENTER);
ui_2d_present(); /* ⚠ 不调这句 = 黑屏 */
/* without this call you get a black screen */
}
static void layout(void) {
SW = ui_scr_get_w();
SH = ui_scr_get_h();
if (SW <= 0) SW = 360;
if (SH <= 0) SH = 620;
}
/* 从剪贴板读一次;读不到就装样例文本(**-1 = 这一端没有剪贴板,不是错误**)
* Read the clipboard once; fall back to sample text when there is none (**-1 means "no clipboard on this
* end", not an error**) */
static void load_clipboard(void) {
int n;
n = ui_clipboard_get(text, TEXT_CAP - 1);
if (n > 0) {
tlen = n;
text[tlen] = 0;
set_status(lang == 0 ? "已从剪贴板读入" : "read from the clipboard", 1);
return;
}
{
char *s = lang == 0 ? "# 内置样例(本机读不到剪贴板)\n hello world \n\n second line \n"
: "# built-in sample (no clipboard here)\n hello world \n\n second line \n";
int i = 0;
while (s[i] != 0 && i < TEXT_CAP - 1) { text[i] = s[i]; i = i + 1; }
tlen = i;
text[tlen] = 0;
set_status(lang == 0 ? "剪贴板读不到,用的是内置样例" : "no clipboard; using the built-in sample", 2);
}
}
int main(void) {
int msg[4];
int t;
int t0;
int done;
lang = ui_scr_get_language();
status[0] = 0;
status_kind = 0;
layout();
ui_win_open_ex("clipboard.c", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
layout();
load_clipboard();
draw();
t0 = ui_time_get_ms();
done = 0;
while (!done && ui_win_is_closed() == 0) {
t = ui_msg_wait(msg, 200);
if (t == VML_MSG_NONE) {
if (ui_time_get_ms() - t0 > IDLE_EXIT_MS) done = 1; /* 空闲超时(验收用)*/
/* idle timeout (for automated checks) */
continue;
}
t0 = ui_time_get_ms();
if (t == VML_MSG_KEYDOWN) {
int k = msg[1];
if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') done = 1;
else if (k == '1') { apply(0); draw(); }
else if (k == '2') { apply(1); draw(); }
else if (k == '3') { apply(2); draw(); }
else if (k == '4') { apply(3); draw(); }
continue;
}
if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
int bw = (SW - 20 - 16) / 3;
int i;
if (bw < 60) bw = 60;
for (i = 0; i < NB; i++) {
int x = btn_x(i, bw);
int y = btn_y(i);
int w = btn_w(i, bw);
if (msg[1] >= x && msg[1] < x + w && msg[2] >= y && msg[2] < y + 38) {
apply(i);
draw();
break;
}
}
continue;
}
if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) { layout(); draw(); continue; }
if (t == VML_MSG_WINDOWCLOSE) done = 1;
}
return 0;
}