#include #include #include #include #if defined(_WIN32) || defined(_WIN64) #else #include #include #include #include #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 }