Files
cKit/Foundation/c_Console.c
T

572 lines
19 KiB
C
Raw Normal View History

2026-08-29 01:50:50 +08:00
#include <c_Console.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(_WIN32) || defined(_WIN64)
#else
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#endif
/* ------------------------------------------------------------------------------------------------------------------ */
/* */
// 仅在 POSIX (Linux/macOS) 平台下使用的全局变量,用于恢复终端
#if defined(_WIN32) && defined(_WIN64)
// 备份用户进入程序前的原始代码页
static UINT orig_in_cp = 0;
static UINT orig_out_cp = 0;
#else
static struct termios orig_termios;
static int is_terminal_initialized = 0;
#endif
// 内部私有回调:用于程序退出时自动恢复终端属性
static void c_Console_ResetOnExit(void) {
#if defined(_WIN32) || defined(_WIN64)
// 恢复 Windows 用户原本的代码页环境,避免污染用户的 CMD/PowerShell 终端
if (orig_in_cp != 0) SetConsoleCP(orig_in_cp);
if (orig_out_cp != 0) SetConsoleOutputCP(orig_out_cp);
#else
// Linux 恢复原始 Raw Mode 设置(参考上一轮代码)
extern void disable_raw_mode(void);
disable_raw_mode();
#endif
}
/**
* @brief 跨平台初始化控制台环境
* Windows: 激活全局 ANSI 转义序列支持
* Linux/macOS: 关闭行缓冲、关闭按键回显,并注册退出恢复钩子
*/
void c_Console_Init(void) {
// 1. 全平台通用的 C 标准库本地化声明(促使 printf/scanf 内部行为适配 UTF-8
setlocale(LC_ALL, ".UTF-8");
#if defined(_WIN32) || defined(_WIN64)
// ----------------------------------------------------
// Windows 平台:激活 VT 模式并全面强制 UTF-8 (65001)
// ----------------------------------------------------
// 备份旧代码页
orig_in_cp = GetConsoleCP();
orig_out_cp = GetConsoleOutputCP();
// 强行设为 UTF-8 编码(65001 == CP_UTF8
SetConsoleCP(CP_UTF8); // 影响控制台输入(如 scanf/fgets
SetConsoleOutputCP(CP_UTF8); // 影响控制台输出(如 printf)
// 注册退出钩子,以便程序正常退出或 exit 时自动还原环境
atexit(c_Console_ResetOnExit);
// 激活虚拟终端 (VT) 从而支持 ANSI 转义序列
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut != INVALID_HANDLE_VALUE) {
DWORD dwMode = 0;
if (GetConsoleMode(hOut, &dwMode)) {
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOut, dwMode);
}
}
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
if (hIn!=INVALID_HANDLE_VALUE) {
DWORD mode;
// 1. 获取当前控制台的输入模式
if (GetConsoleMode(hIn, &mode)) {
// 2. 启用鼠标输入
mode |= ENABLE_MOUSE_INPUT;
// 3. 禁用快速编辑模式(非常关键!如果不禁用,鼠标事件会被控制台自身拦截用于复制文本)
mode &= ~ENABLE_QUICK_EDIT_MODE;
// 4. 启用扩展属性(如果要修改 QUICK_EDIT,必须同时带上这个标志)
mode |= ENABLE_EXTENDED_FLAGS;
// 5. 应用新模式
SetConsoleMode(hIn, mode);
}
}
#else
// ----------------------------------------------------
// Linux/macOS 平台:开启 Raw Mode(沿用上一轮逻辑)
// ----------------------------------------------------
extern struct termios orig_termios;
extern int is_terminal_initialized;
if (!isatty(STDIN_FILENO)) return;
if (tcgetattr(STDIN_FILENO, &orig_termios) < 0) return;
atexit(c_Console_ResetOnExit);
is_terminal_initialized = 1;
struct termios raw = orig_termios;
raw.c_lflag &= ~(ICANON | ECHO); // 禁用回显和标准缓冲
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
#endif
// 确保标准输出缓冲区立即刷新
fflush(stdout);
}
/* ------------------------------------------------------------------------------------------------------------------ */
/* */
int c_Console_kbhit(void) {
#if defined(_WIN32) || defined(_WIN64)
return _kbhit();
#else
struct timeval tv = {0, 0};
fd_set fds;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
return select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv) > 0;
#endif
}
int c_Console_getch(void) {
#if defined(_WIN32) || defined(_WIN64)
return _getch();
#else
char ch = 0;
if (read(STDIN_FILENO, &ch, 1) < 0) return 0;
return ch;
#endif
}
void c_Console_Sleep(int milliseconds) {
#if defined(_WIN32) || defined(_WIN64)
Sleep(milliseconds);
#else
usleep(milliseconds * 1000);
#endif
}
/* ------------------------------------------------------------------------------------------------------------------ */
/* */
void c_Console_BlankLine(int y) {
// 1. 将光标移动到目标行的第 1 列
// \033[%d;1H : 移动到第 y 行,第 1 列
printf("\033[%d;1H", y);
// 2. 清除从光标位置到行尾的所有内容(现代终端推荐直接用 2K 清除整行)
// \033[2K : 清除光标所在的整行内容,但光标位置保持不变
printf("\033[2K");
// 3. 强制刷新输出缓冲区,确保屏幕立刻更新
fflush(stdout);
}
/**
* @brief 从指定坐标 (x, y) 开始清空到该行的末尾
*/
void c_Console_BlankLineFrom(int x, int y) {
printf("\033[%d;%dH", y, x); // 移动到 (x, y)
printf("\033[0K"); // 清除从当前光标到行尾
fflush(stdout);
}
/**
* @brief 跨平台移动控制台光标到指定坐标
* @param x 目标列坐标 (Column/Horizontal),从 1 开始计,自左向右递增
* @param y 目标行坐标 (Row/Vertical),从 1 开始计,自上向下递增
*/
void c_Console_GotoXY(int x, int y) {
// 防御性保护:ANSI 坐标必须从 1 开始
if (x < 1) x = 1;
if (y < 1) y = 1;
// \033[%d;%dH : 第一个参数是行(y),第二个参数是列(x)
// 这是 ANSI X3.64 标准的固定顺序,切勿将 x 和 y 的位置颠倒
printf("\033[%d;%dH", y, x);
// 强制刷新缓冲区,确保光标立即移动到位
fflush(stdout);
}
/* ------------------------------------------------------------------------------------------------------------------ */
/* */
void c_Console_GetSize(int *width, int *height) {
if (!width || !height) return;
#if defined(_WIN32) || defined(_WIN64)
CONSOLE_SCREEN_BUFFER_INFO csbi;
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(hOut, &csbi)) {
// srWindow 存储了当前可视窗口的矩形边界(0-based 坐标)
*width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
*height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
} else {
*width = 80; // 失败时的兜底默认值
*height = 25;
}
#else
struct winsize w;
// 使用 ioctl 的 TIOCGWINSZ 标志直接获取终端窗口大小
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) {
*width = w.ws_col;
*height = w.ws_row;
} else {
*width = 80; // 失败时的兜底默认值
*height = 25;
}
#endif
}
/* ------------------------------------------------------------------------------------------------------------------ */
/* */
int c_Console_GetVisualWidth(const char *str) {
if (!str) return 0;
int visual_width = 0;
int i = 0;
while (str[i] != '\0') {
unsigned char c = (unsigned char)str[i];
if (c < 0x80) {
// ----------------------------------------------------
// 1. ASCII 字符 (0x00 - 0x7F) -> 占 1 字节,视觉宽度 1
// ----------------------------------------------------
visual_width += 1;
i += 1;
}
else if ((c & 0xE0) == 0xC0) {
// ----------------------------------------------------
// 2. 2字节 UTF-8 字符 (如部分拉丁文、希腊字母) -> 视觉宽度 1
// ----------------------------------------------------
visual_width += 1;
i += 2;
}
else if ((c & 0xF0) == 0xE0) {
// ----------------------------------------------------
// 3. 3字节 UTF-8 字符 (绝大多数中日韩汉字、常用标点) -> 视觉宽度 2
// ----------------------------------------------------
visual_width += 2;
i += 3;
}
else if ((c & 0xF8) == 0xF0) {
// ----------------------------------------------------
// 4. 4字节 UTF-8 字符 (如 Emoji 表情、生僻字) -> 视觉宽度 2
// ----------------------------------------------------
visual_width += 2;
i += 4;
}
else {
// 异常安全降级处理
i += 1;
}
}
return visual_width;
}
void c_Console_PrintCenter(int y, int box_width, const char *str) {
if (!str) return;
// 1. 计算文本的实际视觉宽度
int text_width = c_Console_GetVisualWidth(str);
// 2. 计算居中所需的左侧起始 X 坐标 (1-based 坐标系)
int start_x = (box_width - text_width) / 2 + 1;
if (start_x < 1) start_x = 1; // 边界防御
// 3. 跨平台移动光标并打印
// 注意:需要使用外部已实现的 c_Console_GotoXY,此处以标准 ANSI 语法演示
printf("\033[%d;%dH%s", y, start_x, str);
fflush(stdout);
}
void c_Console_PrintLeftAligned(const char *str, int align_len) {
if (!str) return;
// 打印文本
printf("%s", str);
// 计算实际占用的宽度,并动态补齐缺失的物理空格
int text_width = c_Console_GetVisualWidth(str);
int padding = align_len - text_width;
for (int i = 0; i < padding; i++) {
putchar(' ');
}
}
int c_Console_WriteXY(int x, int y, const char *format, ...) {
// 1. 防御性保护:确保坐标合法
if (x < 1) x = 1;
if (y < 1) y = 1;
// 2. 移动光标到目标坐标 (使用 ANSI CUP 序列)
printf("\033[%d;%dH", y, x);
// 3. 处理 C 语言可变参数并安全输出
va_list args;
va_start(args, format);
int result = vprintf(format, args); // 核心:使用 vprintf 将参数包直接输出到控制台
va_end(args);
// 4. 强制刷新输出缓冲区,确保文本和光标立即更新,防止画面滞后
fflush(stdout);
return result;
}
int c_Console_WriteColorXY(int x, int y, c_ConsoleColor_t fg, c_ConsoleColor_t bg, const char *format, ...) {
// 1. 防御性保护:确保坐标合法
if (x < 1) x = 1;
if (y < 1) y = 1;
// 2. 移动光标到指定位置
printf("\033[%d;%dH", y, x);
// 3. 构建并发送 ANSI 颜色控制序列
// 标准前景色: 30-37, 高亮前景色: 90-97
// 标准背景色: 40-47, 高亮背景色: 100-107
if (fg != C_COLOR_NONE) {
if (fg < 8) {
printf("\033[%dm", 30 + fg); // 标准前景色
} else {
printf("\033[%dm", 90 + (fg - 8)); // 高亮前景色
}
}
if (bg != C_COLOR_NONE) {
if (bg < 8) {
printf("\033[%dm", 40 + bg); // 标准背景色
} else {
printf("\033[%dm", 100 + (bg - 8));// 高亮背景色
}
}
// 4. 处理可变参数并安全输出文本
va_list args;
va_start(args, format);
int result = vprintf(format, args);
va_end(args);
// 5. 关键:格式化文本打印结束后,**必须重置颜色**,否则会污染后续的所有打印
printf("\033[0m");
// 6. 强制刷新输出缓冲区
fflush(stdout);
return result;
}
/* ------------------------------------------------------------------------------------------------------------------ */
/* */
void c_Console_EnableMouse(void) {
#if defined(_WIN32) || defined(_WIN64)
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
DWORD dwMode = 0;
GetConsoleMode(hIn, &dwMode);
// 启用鼠标输入标志
dwMode |= ENABLE_MOUSE_INPUT;
// 如果启用了快速编辑模式(QuickEdit),会导致鼠标点击被系统拦截变成选中文字,必须关闭
dwMode &= ~ENABLE_QUICK_EDIT_MODE;
SetConsoleMode(hIn, dwMode);
#else
// Linux/macOS: 发送 ANSI 序列开启鼠标追踪
// \033[?1003h: 追踪所有鼠标动作(包括移动、点击、释放)
// \033[?1006h: 启用 SGR 鼠标编码模式(现代终端标配,坐标支持超过 255)
printf("\033[?1003h\033[?1006h");
fflush(stdout);
#endif
}
void c_Console_DisableMouse(void) {
#if !defined(_WIN32) && !defined(_WIN64)
printf("\033[?1003l\033[?1006l"); // 关闭鼠标追踪
fflush(stdout);
#endif
}
/**
* @brief 跨平台非阻塞读取鼠标事件
* @param mouse_evt 用于接收转换后的通用鼠标事件结构体
* @return int 如果成功捕获到鼠标事件返回 1,否则返回 0
*/
int c_Console_ReadMouse(c_ConsoleMouseEvent_t *mouse_evt) {
if (!mouse_evt) return 0;
mouse_evt->type = MOUSE_EVENT_NONE;
#if defined(_WIN32) || defined(_WIN64)
// ----------------------------------------------------
// Windows 平台实现
// ----------------------------------------------------
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
DWORD numEvents = 0;
// 检查输入缓冲区中是否有事件
GetNumberOfConsoleInputEvents(hIn, &numEvents);
if (numEvents == 0) return 0;
INPUT_RECORD inRec;
DWORD numRead = 0;
// 窥视而不直接移出,先确认是不是鼠标事件
PeekConsoleInput(hIn, &inRec, 1, &numRead);
if (numRead > 0 && inRec.EventType == MOUSE_EVENT) {
// 确实是鼠标事件,正式读出
ReadConsoleInput(hIn, &inRec, 1, &numRead);
MOUSE_EVENT_RECORD mer = inRec.Event.MouseEvent;
// 转换坐标 (Windows 是 0-based,我们要统一转换为 1-based)
mouse_evt->x = mer.dwMousePosition.X + 1;
mouse_evt->y = mer.dwMousePosition.Y + 1;
// 判断事件类型
if (mer.dwEventFlags == 0) {
// 点击或释放
if (mer.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED) {
mouse_evt->type = MOUSE_EVENT_PRESS_LEFT;
} else if (mer.dwButtonState & RIGHTMOST_BUTTON_PRESSED) {
mouse_evt->type = MOUSE_EVENT_PRESS_RIGHT;
} else if (mer.dwButtonState & FROM_LEFT_2ND_BUTTON_PRESSED) {
mouse_evt->type = MOUSE_EVENT_PRESS_MIDDLE;
} else {
mouse_evt->type = MOUSE_EVENT_RELEASE;
}
} else if (mer.dwEventFlags == MOUSE_WHEELED) {
// 滚轮滚动 (HIWORD 为正向上,为负向下)
if ((short)HIWORD(mer.dwButtonState) > 0) {
mouse_evt->type = MOUSE_EVENT_WHEEL_UP;
} else {
mouse_evt->type = MOUSE_EVENT_WHEEL_DOWN;
}
} else if (mer.dwEventFlags == MOUSE_MOVED) {
mouse_evt->type = MOUSE_EVENT_MOVE;
}
return 1;
} else if (numRead > 0) {
// 如果不是鼠标事件(比如是键盘事件),则丢弃或由其他函数处理
ReadConsoleInput(hIn, &inRec, 1, &numRead);
}
#else
// ----------------------------------------------------
// Linux/macOS 平台实现 (解析 SGR 鼠标协议序列)
// SGR 格式一般为: \033[<按钮代号>;X坐标;Y坐标M (或 m 代表释放)
// ----------------------------------------------------
if (!c_Console_kbhit()) return 0;
int ch = c_Console_getch();
if (ch == 27) { // 捕获到 ESC (\033)
if (c_Console_getch() == '[') {
if (c_Console_getch() == '<') {
int btn = 0, x = 0, y = 0;
char action = 0;
// 解析 SGR 密文参数,例如 "0;45;12M"
// 现代终端标准,直接使用 scanf 变体或手动循环读取
if (scanf("%d;%d;%d%c", &btn, &x, &y, &action) == 4) {
mouse_evt->x = x;
mouse_evt->y = y;
if (action == 'm') {
mouse_evt->type = MOUSE_EVENT_RELEASE;
} else if (action == 'M') {
if (btn == 0) mouse_evt->type = MOUSE_EVENT_PRESS_LEFT;
else if (btn == 1) mouse_evt->type = MOUSE_EVENT_PRESS_MIDDLE;
else if (btn == 2) mouse_evt->type = MOUSE_EVENT_PRESS_RIGHT;
else if (btn == 32) mouse_evt->type = MOUSE_EVENT_MOVE; // 伴随左键的移动
else if (btn == 35) mouse_evt->type = MOUSE_EVENT_MOVE; // 纯移动
else if (btn == 64) mouse_evt->type = MOUSE_EVENT_WHEEL_UP;
else if (btn == 65) mouse_evt->type = MOUSE_EVENT_WHEEL_DOWN;
}
return 1;
}
}
}
}
#endif
return 0;
}
/* ------------------------------------------------------------------------------------------------------------------ */
/* */
/**
* @brief 跨平台精准读取并转换键盘按键(包含方向键、Esc、Enter)
* @note 必须确保终端已通过 c_Console_Init() 切换到非阻塞/Raw 模式
* @return int 返回转换后的统一 c_ConsoleKeyCode_t 键值,若为普通字符则原样返回其 ASCII 码
*/
int c_Console_ReadKey(void) {
if (!c_Console_kbhit()) return C_KEY_UNKNOWN;
int ch = c_Console_getch();
#if defined(_WIN32) || defined(_WIN64)
// ----------------------------------------------------
// Windows 平台:处理扩展键双字节
// ----------------------------------------------------
if (ch == 0 || ch == 224) {
// 遇到特殊前缀,必须紧接着读取第二个字节
int sub_ch = _getch();
switch (sub_ch) {
case 72: return C_KEY_UP;
case 80: return C_KEY_DOWN;
case 75: return C_KEY_LEFT;
case 77: return C_KEY_RIGHT;
default: return C_KEY_UNKNOWN;
}
}
// Windows 的回车可能返回 '\r' (13),统一规范化
if (ch == '\r' || ch == '\n') return C_KEY_ENTER;
return ch; // 普通 ASCII 字符 (如 'w', 'a', 's', 'd') 直接原样返回
#else
// ----------------------------------------------------
// Linux/macOS 平台:处理 ANSI 键盘转义序列流
// ----------------------------------------------------
if (ch == 27) { // 捕获到第一个字节是 ESC
// 立即检查缓冲区,看后面有没有跟着字符。如果没有,说明用户真的只按了独立的 Esc 键
if (!c_Console_kbhit()) {
return C_KEY_ESC;
}
int next1 = c_Console_getch();
if (next1 == '[') {
if (!c_Console_kbhit()) return C_KEY_UNKNOWN;
int next2 = c_Console_getch();
// 标准方向键序列判定: ESC [ A/B/C/D
switch (next2) {
case 'A': return C_KEY_UP;
case 'B': return C_KEY_DOWN;
case 'C': return C_KEY_RIGHT;
case 'D': return C_KEY_LEFT;
default: return C_KEY_UNKNOWN;
}
}
return C_KEY_UNKNOWN;
}
// Linux 退格键适配
if (ch == 127) return C_KEY_BACKSPACE;
// 回车键统一
if (ch == '\n' || ch == '\r') return C_KEY_ENTER;
return ch; // 普通 ASCII 字符原样返回
#endif
}