Files
2026-08-30 02:51:25 +08:00

572 lines
19 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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
}